Compare commits

...

1028 Commits

Author SHA1 Message Date
Karan
07af5f91c8 chore(license): Change license from GPL-3 to MIT (#4339)
* Change license from GPL-3 to MIT

Updated the project license from GPL-3 to MIT in DESCRIPTION, LICENSE, LICENSE.md, README.md, and package.json. Added LICENSE.md with the MIT license text and updated .Rbuildignore to exclude LICENSE.md from builds.

* `npm run build` (GitHub Actions)

* Update LICENSE and add LICENSE.note

Replaced the LICENSE file content with a summary including year and copyright holder. Moved detailed third-party license information to a new LICENSE.note file.

* Remove R check log file

Deleted the ..Rcheck/00check.log file, likely to clean up generated or temporary files from the repository.
2025-12-16 17:51:22 -06:00
Barret Schloerke
fda6a9fede chore(assets): Update asset versions (#4337) 2025-12-11 11:56:42 -05:00
Barret Schloerke
d2245a2e34 Increment version number to 1.12.1.9000 2025-12-09 16:29:27 -05:00
Barret Schloerke
a12a8130b8 v1.12.1 (#4329) 2025-12-09 16:26:52 -05:00
Barret Schloerke
b436d2a96d Clarify OTel collection level usage in docs (#4335)
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2025-12-08 15:31:57 -05:00
Barret Schloerke
05b0f270c4 fix(otel): ExtendedTask's otel enabled status set during init (#4334) 2025-12-08 14:55:59 -05:00
Barret Schloerke
f24f71e4e0 feat(otel): Add withOtelCollect() and localOtelCollect() (#4333) 2025-12-08 14:30:40 -05:00
Barret Schloerke
63a00f775f fix(otel): Duplicate otel code attribute keys using both deprecated and preferred names (#4325) 2025-12-03 16:37:20 -05:00
Barret Schloerke
5a946caf35 Skip timer tests on CRAN and fix empty vector comparison (#4327) 2025-12-03 16:29:17 -05:00
Barret Schloerke
16c016a171 Increment version number to 1.12.0.9000 2025-12-03 15:50:36 -05:00
Barret Schloerke
284af65534 Update .Rbuildignore 2025-12-03 15:50:27 -05:00
Barret Schloerke
b5da7868fa v1.12.0 (#4312) 2025-12-03 15:48:49 -05:00
ismirsehregal
c8a41aa834 Update dateYMD (#4318)
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2025-12-01 10:25:28 -05:00
Copilot
390f6d3b95 chore(otel): Rename shiny.otel.bind to shiny.otel.collect (#4321)
Co-authored-by: Barret Schloerke <barret@posit.co>
2025-11-25 16:36:56 -05:00
Barret Schloerke
9a2140cd19 chore(test): Fix stacks test with R 4.5.2 (#4322) 2025-11-25 16:31:53 -05:00
Barret Schloerke
e3cf4fb089 refactor(otel): Cache tracer and logger on init and on demand (#4315)
Co-authored-by: Charlie Gao <53399081+shikokuchuo@users.noreply.github.com>
2025-11-11 08:44:57 -05:00
Barret Schloerke
472a1cdba1 refactor(otel): Refactor internal method names (#4313) 2025-11-06 11:20:37 -05:00
Barret Schloerke
b56c275364 feat(otel): Enhanced OpenTelemetry support (#4300) 2025-10-28 14:01:50 -04:00
Garrick Aden-Buie
592e825a0f feat: Replace client-side markdown rendering with server-side in showcase mode (#4306)
* feat: Replace client-side markdown rendering with server-side in showcase mode

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

* chore: callout rendering differences may happen
2025-10-22 09:34:20 -04:00
Barret Schloerke
50a140c580 ci(test): Disable installation of Cairo for unit tests (#4301) 2025-10-15 16:24:22 -04:00
Barret Schloerke
48d255a235 feat: Add {otel} support (#4269) 2025-10-14 15:40:36 -04:00
Barret Schloerke
a01fcc5194 chore(check): Fix minor check warnings (#4299) 2025-10-10 16:30:47 -04:00
Winston Chang
b6e9e9d216 Update package.json for publishing (#4284)
* Update package.json for publishing

* Use custom readme file for npm

* Update repository URLs

* Script cleanup

* Use name @posit/shiny
2025-09-09 16:37:06 -05:00
Barret Schloerke
5ddb99a5b4 update docs (#4285) 2025-09-05 13:52:30 -04:00
Carson Sievert
f981ed6363 feat: add shinyRemoveButton selectize.js option (for py-shiny) (#4276)
* fix: supply and retain default selectize.js plugins (for py-shiny)

* `npm run build` (GitHub Actions)

* Move more in a 'remove_button' attribute direction

* `npm run build` (GitHub Actions)

* Move to a JSON-only approach

* `npm run build` (GitHub Actions)

* Drop sticky update logic by always sending 'missing' value and resolving client-side

* Cleanup

* Don't mutate options; better typing pattern

* `npm run build` (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-08-28 15:49:25 -05:00
Carson Sievert
6d6b0ea6f9 fix: add CSS workaround to hide all but the first selectize remove button (#4275)
* fix: add CSS workaround to hide all but the first selectize remove button

* Update news

* Apply suggestions from code review
2025-08-21 16:48:34 -05:00
Barret Schloerke
0e355ed25c preemptively run revdepcheck. 5 failures. 2025-08-18 09:10:19 -04:00
Barret Schloerke
80a9ff470c fix(scss): Remove updated scss files and restore R sass compilation (#4272)
* Revert changes in 8861645d99 from #4252

We compile from R sass. So we should be partial to R's compiler, no the latest in npm

* Restore original compile script (with message) that built using R-sass, not npm

* Add step in build command to update the sass output

* run new command
2025-08-14 16:03:48 -04:00
Barret Schloerke
ead0abcd62 bug(check): Update .Rbuildignore with new and removed files (#4270) 2025-08-04 23:17:45 -04:00
Barret Schloerke
7dcb54bc7e build(js): Migrate from yarn to npm (#4252) 2025-08-04 16:07:39 -04:00
Colin Gillespie
ae82850e1f docs: Update R supported versions (#4256) 2025-08-04 10:37:48 -04:00
Daniel Chen
0610d756a8 docs: update srcts/readme.md to shiny 1.11.1 (#4259) 2025-08-04 09:50:06 -04:00
Carson Sievert
6325067130 Update DESCRIPTION (#4255)
* Change maintainer to Carson

* Add Garrick

* Add Posit ROR link

* `usethis::use_tidy_description()` (GitHub Actions)

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

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-07-22 13:19:24 -05:00
Carson Sievert
1a4e52dc73 Run routine (#4257)
* Touch news

* `usethis::use_tidy_description()` (GitHub Actions)

* Revert "Touch news"

This reverts commit 854e7e10ae.

* `usethis::use_tidy_description()` (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-07-22 12:38:23 -05:00
Winston Chang
08383ad8b9 Update srcts/README with info about window.Shiny (#4198) 2025-07-18 17:22:52 -05:00
Carson Sievert
ecf6bfe9a7 Put actionButton()s icon and label into containers (#4249)
* Put action icon and label into containers

* Update snaps

* More robust test

* Don't include container if icon/label isn't specified

* `yarn build` (GitHub Actions)

* Send HTML string/deps on update; update news

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-07-14 16:22:03 -05:00
Carson Sievert
f7528568e5 Increment version number to 1.11.1.9000 (#4253)
* Increment version number to 1.11.1.9000

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-07-07 22:02:14 -07:00
Carson Sievert
51f653b66f v1.11.1 release candidate (#4245)
* v1.11.1 release candidate

* `yarn build` (GitHub Actions)

* Revert actionButton()/actionLink() implementation to v1.11.0's behavior (re-introducing #4239)

* Minimal fix to address the regression in #4239

Ideally we'd fix this issue, and also get updateActionButton() working with HTML labels, but thanks to today's release of kinesis (which snapshots all of actionButton()s markup), and CRAN dragging their feet to accept our original submission (which was fine, by the way), we can't have nice things

* `yarn build` (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-07-07 21:53:08 -07:00
Carson Sievert
460a93a5fd Revert the addition of spacing between icon and label in actionButton() (#4248)
* Revert the addition of spacing between icon and label in actionButton()

* `yarn build` (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-06-27 10:58:17 -05:00
Carson Sievert
3ea4c8eb1d Restrict icon/label separator spacing to actionButton() (#4247)
* Follow up to #4242: Restrict icon/label separator spacing to actionButton()

* `yarn build` (GitHub Actions)

* Add spacing only when both icon and label are truthy

* Update snapshot

* `yarn build` (GitHub Actions)

* Slightly more readable JS

* `yarn build` (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-06-26 19:12:13 -05:00
Carson Sievert
f237de559d Fix front-end action button label updating logic (#4242)
* Close #4239: fix front-end action button label updating logic (follow up to #3996)

* Update news

* Use a separator instead of putting markup in attributes

* `yarn build` (GitHub Actions)

* Address feedback

* Cleanup

* Refactor into a single method to split icon/label

* `yarn build` (GitHub Actions)

* Better naming

* Add some padding to the separator

* Add some unit tests for R logic

* Update NEWS.md

* Update NEWS.md

* Update NEWS.md

* Update NEWS.md

* Increase backcompat (keep same R structure when no icon is provided)

* Refine comment

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-06-26 16:32:17 -05:00
Carson
8c7abbac44 Update news 2025-06-26 13:56:35 -05:00
Carson Sievert
1710316142 Properly handle undefined value for input subscribe callback (#4243)
* Close #4240: properly handle undefined value for input subscribe callback

* Refactor normalization into a function
2025-06-26 09:36:09 -05:00
Carson Sievert
2d856f4f09 Start new version (#4241)
* Start new version

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-06-25 17:05:03 -05:00
Carson Sievert
ab219e3408 v1.11.0 release candidate (#4232) 2025-06-25 16:28:51 -05:00
Carson Sievert
673be3dd77 Follow up to #3996: fix front-end checkbox label updating logic (#4238)
* Follow up to #3996: fix front-end checkbox label updating logic

* More descriptive name
2025-06-20 15:15:30 -05:00
Carson Sievert
b25e6feabb feat(InputBinding): subscribe callback now supports event priority (#4211)
* feat(InputBinding): subscribe callback now supports event priority

* Update NEWS.md

* Update srcts/src/shiny/bind.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* `yarn build` (GitHub Actions)

* Simpler and more consistent typing

* Support a suitable object as input

* Provide a type for the callback itself, not just the valueit's given

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-06-19 10:27:45 -05:00
Carson Sievert
e6b22d86b6 Follow up to #3996 when label is unspecified (i.e., NULL), don't include it in the message (#4237) 2025-06-19 09:48:44 -05:00
Carson Sievert
9c5196ee63 Run routine (#4234)
* Run routine

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

* `yarn build` (GitHub Actions)

* Update NEWS.md

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-06-16 12:52:48 -05:00
Hedley
9b53251b09 chore: #4175 update jquery-ui to 1.14.1 (#4205)
* chore: #4175 update jquery-ui to 1.14.1

* Update to latest types

---------

Co-authored-by: Carson <cpsievert1@gmail.com>
2025-06-16 12:42:07 -05:00
David Macro
942bdd8c40 Update jQuery to 3.7.1 (#3969)
* Update jquery

* Upgrade to latest types

* Update news

---------

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2025-06-16 11:46:17 -05:00
edemain03
d762865753 Fix 404 in example 08_html (shiny.min.css) (#4221)
* fix(examples-shiny): use shiny.min.css in 08_html to avoid 404 (#4220)

* Update NEWS.md

* Minimize all the files

---------

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2025-06-16 11:20:00 -05:00
Carson Sievert
992b967095 Follow up to #3870: fix location of news item (#4233) 2025-06-16 11:12:58 -05:00
Stuart Russell
9a39cea0cc Bugfix for error found in tests (#3870)
* Bugfix for error found when calling shiny::shinyAppTemplate without library(shiny)

* Update news

* Update NEWS.md

---------

Co-authored-by: Carson <cpsievert1@gmail.com>
2025-06-16 11:05:43 -05:00
John Coene
db9f210257 Allow update input labels with HTML (#3996)
* fix: allow update input labels with HTML fixes #3995

* refactor: use processDeps and renderContent

* fix: formatting on lists

* fix: put spaces between infix

* chore: generated files

* fix: update input tests

* revert: generated javascript and sourcemaps

* fix: empty label check

* Remove package-lock

* Undo unintended change when merging

* Update news

* Simplify

---------

Co-authored-by: Carson <cpsievert1@gmail.com>
2025-06-16 11:01:44 -05:00
Charlie Gao
e8b7c08a19 Adds mirai to documentation (#4230)
* Update docs to mention mirai

* Update example to use mirai

* Fix other roxygen2 render
2025-06-16 09:42:21 -05:00
Michael Chirico
b596245571 family->given for R Core authorship (#4222) 2025-06-12 16:23:57 -05:00
Carson Sievert
57bb3a12d3 fix(renderPlot): get interactive plotting working with ggplot2 v4.0 (#4228)
* fix(renderPlot): get interactive plotting working with ggplot2 v4.0

* Update NEWS.md
2025-06-12 16:23:38 -05:00
Winston Chang
219fbc6819 Update NEWS 2025-06-09 17:22:20 -05:00
Teun van den Brand
a660093fa5 Compatibility with ggplot2 4.0.0 (#4226)
* add S7 class method

* Update tests/testthat/test-plot-coordmap.R

---------

Co-authored-by: Winston Chang <winston@stdout.org>
2025-06-09 17:20:43 -05:00
Garrick Aden-Buie
eac0eea886 fix: Wrap extended task invocation in promise_resolve() (#4225)
* fix: Wrap extended task invocation in `promise_resolve()`

* refactor: cleanup error handling and promise chain

* chore: add news entry
2025-05-30 08:44:31 -04:00
Charlie Gao
6df0bb9423 Fix performance regression related to limiting deep call stack growth (#4214)
* Use less expensive version of getCallNames() just for hashing

* Update R/conditions.R

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

---------

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
Co-authored-by: Barret Schloerke <barret@rstudio.com>
2025-05-23 18:56:00 +01:00
Barret Schloerke
159e771ac7 Relax test as we've already confirmed throttle is updating. Only required final value expectation (#4218) 2025-05-23 13:43:03 -04:00
Carson Sievert
ca41c0831b feat!(submitButton): don't treat any HTML type='submit' button/input like a submitButton() (#4209)
* feat!(submitButton): don't treat any HTML type='submit' button/input like a submitButton()

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

* Update NEWS.md

* `yarn build` (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
Co-authored-by: Barret Schloerke <barret@rstudio.com>
Co-authored-by: schloerke <schloerke@users.noreply.github.com>
2025-05-01 12:37:21 -05:00
Carson Sievert
316c3c8409 feat(textAreaInput): Add an autoresize option (#4210)
* feat(textAreaInput): Add an autoresize option

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

* `yarn build` (GitHub Actions)

* Update NEWS.md

* Fix broken CSS selector.

Rules aren't being applied correctly in PyShiny either...

* Put shiny input class on container (to mirror what PyShiny does)

* Refactor autoresize logic

* Reduce diff size

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2025-04-30 18:34:04 -05:00
Garrick Aden-Buie
f79a22b987 feat: Fully reload ui/server when autoreload occurs (#4184)
* feat: Fully reload ui/server when autoreload occurs

* chore: remove stray empty line

* chore: clean up function names and add comments

* docs: Add news item

* feat: Use {watcher} for autoreload file watching (#4185)

* feat: Use {watcher}

* chore: shikokuchuo/watcher@dev

* chore: watcher is on CRAN now

* chore: Undo air format changes

* feat: Use `shiny.autoreload.interval` for watcher latency

* chore: Simply track last time auto-reload changed

* docs: rewrite options docs for clarity

* chore: code style

* docs: global.R changes are not applied

* feat(ui/server): Autoreload also reloads global and R support files

* chore: remove outdated comment

* chore: safer comparisons

* chore: Restore legacy autoreload watcher if {watcher} not installed

* rename: autoload_r_support_if_needed()

* chore: use `rlang::is_false()`

* chore: use_build_ignore("_dev")
2025-04-24 13:53:40 -04:00
Garrick Aden-Buie
83219e3551 fix: Improve jquery node detection (#4203) 2025-03-25 21:26:27 -04:00
Garrick Aden-Buie
f55c26af4a docs: Link to outputOptions() from render functions (#4196)
* docs(downloadHandler): Link to `outputOptions()`

* docs: include `outputOptions()` in other render functions
2025-03-03 11:11:11 -05:00
Garrick Aden-Buie
9fbb2c5829 docs: Rewrite news for #4183 (#4195) 2025-02-28 08:06:54 -05:00
Winston Chang
531f31b66f textInput(), textAreaInput(), numericInput(), passwordInput(): allow updating value on blur (#4183)
* textInput: Add updateOn parameter and allow setting debounce delay

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

* `yarn build` (GitHub Actions)

* Update news

* Remove debounce parameter

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

* `yarn build` (GitHub Actions)

* Add updateOn parameter to numericInput, passwordInput

* Add updateOn to textAreaInput()

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

* feat: Ignore change events unless from server messages when `updateOn="blur"`

* refactor: `updateOn="change"` instead of `"input"`

* feat: Update inputs on Enter or Cmd/Ctrl+Enter (textarea)

* chore: Document `...` and ensure they are empty

* chore: Use `rlang::arg_match()`

* chore: code style (air format)

* fix: textAreaInput, not inputTextArea

* docs(NEWS): Minor edit

* chore: If element has focus, ignore change event

---------

Co-authored-by: wch <wch@users.noreply.github.com>
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2025-02-26 12:45:26 -05:00
Winston Chang
58e152154a Stop using Babel; compile JS to ES2020 (#4066)
Co-authored-by: wch <wch@users.noreply.github.com>
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
Co-authored-by: gadenbuie <gadenbuie@users.noreply.github.com>
Co-authored-by: Barret Schloerke <barret@posit.co>
2025-02-03 12:37:19 -05:00
Garrick Aden-Buie
55b37fdeb3 fix(insertTab): Render inserted nav html only once (#4179)
* fix: Fix checking if `scope` is a jquery element

Fixes rstudio/bslib#1159

* refactor: Don't check binding validity if `scope` isn't an element

* fix(insertTab): Render inserted nav html only once

* chore: Don't need to delay binding

* fix: Bind all after inserting nav controls

Output bindings require outputs to be attached to the DOM.

* chore: align comment

* chore: Add news item
2025-01-27 17:15:27 -05:00
Garrick Aden-Buie
b8a5aef53a feat: De-duplicate client console messages (#4177)
* feat: De-duplicate client console messages

* refactor(ShinyErrorConsole): Add `appendConsoleMessage()` static method

* fix: Make `appendConsoleMessage()` an instance method

* rename: `createClientMessageElement()`

* docs: add news item
2025-01-27 16:57:10 -05:00
Carson Sievert
d764ea9b4e Busy indicator improvements (#4172)
* Make sure spinner is visible when htmlwidget errors are visible

* Give recalculating outputs a min-height large enough to show the spinner

* tableOutput() now gets the spinner treatment

* yarn run bundle_extras

* Forward visibility hidden for all recalculating widgets, not just those with a error message (otherwise spinner won't be visible after a req())

* Update news
2025-01-22 14:14:20 -06:00
olivroy
8ad779f949 Various test lints (#4171)
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2025-01-21 15:08:00 -05:00
olivroy
7642fc84b7 Replace crayon by cli + address some TODOs to add some color (#4170)
* Replace crayon by cli + address some TODOs to add some color

* docs: add news

---------

Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2025-01-21 11:00:51 -05:00
Garrick Aden-Buie
0952f3e0a7 ci: update for 2025 (#4178)
* ci: update for 2025

* chore: RStudio --> Posit
---------

Co-authored-by: gadenbuie <gadenbuie@users.noreply.github.com>
2025-01-14 13:26:41 -05:00
Garrick Aden-Buie
13ca8dfc57 fix: Schedule .modal("hide") for transitioning modals (#4173)
* fix: Schedule modal removal for transitioning modals

* refactor: Simplify and both call and schedule modal hiding at same time

* Increment version number to 1.10.0.9000

* docs: Add NEWS entry
2024-12-31 16:27:25 -05:00
Garrick Aden-Buie
79f42f5846 v1.10.0 (#4166)
* Increment version number to 1.10.0

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

* chore: urlchecker::url_update()

✔ Updated: <https://shiny.rstudio.com/reference/shiny/latest/insertUI.html> to <https://shiny.posit.co/r/reference/shiny/latest/insertui.html> in NEWS.md
✔ Updated: <https://shiny.rstudio.com/reference/shiny/latest/modalDialog.html> to <https://shiny.posit.co/r/reference/shiny/latest/modaldialog.html> in NEWS.md
✔ Updated: <https://shiny.rstudio.com/reference/shiny/latest/Progress.html> to <https://shiny.posit.co/r/reference/shiny/latest/progress.html> in NEWS.md
✔ Updated: <https://shiny.rstudio.com/reference/shiny/latest/renderTable.html> to <https://shiny.posit.co/r/reference/shiny/latest/rendertable.html> in NEWS.md
✔ Updated: <https://shiny.rstudio.com/reference/shiny/latest/showNotification.html> to <https://shiny.posit.co/r/reference/shiny/latest/shownotification.html> in NEWS.md
✔ Updated: <https://shiny.rstudio.com/reference/shiny/latest/withProgress.html> to <https://shiny.posit.co/r/reference/shiny/latest/withprogress.html> in NEWS.md

* chore: revdepcheck

* chore: polish NEWS

* chore: update CRAN comments

* Polish NEWS

---------

Co-authored-by: gadenbuie <gadenbuie@users.noreply.github.com>
Co-authored-by: Carson <cpsievert1@gmail.com>
2024-12-16 09:30:53 -06:00
Joe Cheng
9a35b01e23 Fix observeEvent stack trace stripping (#4163)
* Fix observeEvent stack trace stripping

* Add unit test

* Add deep stack version of unit test
2024-12-09 20:50:53 -08:00
Joe Cheng
5bf0701939 Plot outputs incorrectly sized inside scaled outputs (#4139)
* Fix #4135: Plot outputs incorrectly sized inside scaled outputs

CSS zoom property affects el.getBoundingClientRect() but not
el.offsetWidth/Height. When reporting sizes of outputs from
client to server, we need to back out the CSS zoom because
those sizes are used as CSS width/height, which will be
affected by zoom.

(Note that something similar happens with CSS transforms but
we don't have a good way to deal with them)

* Squelch TS error

* `yarn build` (GitHub Actions)

* Add TODO

Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>

* Rebuild JS

---------

Co-authored-by: jcheng5 <jcheng5@users.noreply.github.com>
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2024-12-08 01:25:32 -08:00
Garrick Aden-Buie
e5083f4938 feat: Avoid throwing errors for shared input/output IDs (#4101)
* refactor: Factor out message display from error handler

* feat: Add custom event for sending a client message

* feat: Report binding validity problem via event instead of throwing error

* feat: Don't need to hide shared input/output message

Now that it's not an error, it's safe to report

* refactor: Move `inDevMode()` logic into error console

* refactor: Rename `.error` --> `.event`

* feat: wrap client error message

It's otherwise hard to tell that the error is scrollable
Plus the scrolling is over the whole message rather than the part that overflows

* feat: always send client console messages to browser console as well

* chore: throw if `shiny:client-message` receives an event that isn't CustomEvent

* feat: Handle status in `showShinyClientMessage()`

* Renamed `showMessageInClientConsole()` to `showShinyClientMessage()` to improve clarity

* Added `status` argument to `showShinyClientMessage()` to allow for different message types

* refactor: Don't throw errors for duplicate IDs

Brings dev mode in line with current "prod" behavior,
where errors aren't thrown for duplciates. In both cases
we still get console or client messages.

* refactor: Clean up `status` inside `checkValidity()`

* refactor: Have `checkValidity()` handle emitting the client console event
2024-12-06 16:00:19 -05:00
Garrick Aden-Buie
ce6a562a3c chore: routine (#4161) 2024-12-06 15:16:29 -05:00
Dan Gealow
b6bcfc8683 Work around a selectize bug (re-fixes #3966) (#4142)
* Work around a selectize bug (re-fixes #3966)

---------

Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2024-12-06 15:03:59 -05:00
Dean Attali
d37beeece7 ExtendedTask: add example to docs (#4087)
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2024-12-06 14:24:01 -05:00
Joe Cheng
79ee25620f Limit deep stack growth (#4156)
* Limit deep stack growth

* Improvements to deep stack trace culling

- Keep around the first deep stack trace; it may have useful
  information. (We may want to change this in the future to
  keep the first two stack traces, or even make it an option)
- Print out an indicator that we've elided stack traces, and
  how many

* Add comments

* Add NEWS item

* Add test for unlimited deep stacks

* Code review feedback

* Code review feedback

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

* Use head() over indexing

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

* Improve unit test robustness

* Remove vector indices from snapshot

* Make stack trace stripping work across deep stacks

* Pass tests

* Try passing tests again

* Rename keep_head to retain_first_n

* Remove misleading variable assignment

* Add more comments, refine dropTrivialTestFrames

* Don't call stripStackTraces if we're not stripping

* Use deep stack deduplication instead of elision

This hopefully will avoid any potential ..stacktraceon../off..
scoring issues, and will be more useful for users. The downside
is that it's still possible to have uselessly large deep stack
traces, but at least that will only happen now if you have
manually written gigantic async/promise chains by hand or maybe
did some clever metaprogramming. The coro case should be fine.

* Add coro-based unit test

* Use rlang::hash, it's much faster

* typo

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

* Remove unnecessary logic

* Simplify/robustify reactlog version checking test

* Warn only once on call stack digest cache miss

* Super conservatively wrap appendCallStackWithDupe in try/catch

* Use more specific attribute name

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

* Remove excessively cautious try/catch

---------

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2024-12-06 10:17:05 -08:00
Joe Cheng
82c678a1eb Update NEWS.md for stack trace domain explosion 2024-12-03 13:37:58 -08:00
Joe Cheng
458924569a Stack trace domain explosion (#4155)
* Avoid way too many promise domains being activated

Using `captureStackTraces` in wrapForContext is a bad idea, it
piles on a new domain every time a handler is bound.

* Use captureStackTraces, it means the same thing

* Update promises version requirement

* Add test for stack trace growth

* Simplify stack trace snapshot tests

The `category` column isn't a good candidate for snapshot
testing, as its contents vary depending on how the package
was loaded/installed. During devtools::test() or similar,
shiny package code shows up as 'user'. But during CI, it
doesn't show up as anything.
2024-12-03 13:23:50 -08:00
Garrick Aden-Buie
501b012b2b chore: Remove zipfs from recommended vscode extensions (#4151) 2024-11-07 10:52:10 -05:00
Garrick Aden-Buie
ee1aac847a docs(runExample): display.mode follows DESCRIPTION (#4152)
Fixes #4077

The argument documentation wasn't updated to reflect that `runExample()` can run more than just Shiny's examples
2024-11-07 10:51:27 -05:00
bart1
7785a76a67 Closing bracket missing (#4150)
* type

* typo
2024-11-01 09:05:20 -05:00
Adam Foryś
79af1d6c92 Fix url bookmarking with possibility to modify excludes (#3762)
* Fix url bookmarking with possibility to modify excludes

* Update NEWS.md
2024-10-28 09:36:42 -05:00
Yihui Xie
a145add5d4 Use double-tilde for strikethrough in Markdown tests (#4144)
* Use double-tilde for strikethrough in Markdown tests

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

* Skip tests if commonmark is outdated

* Revert "Skip tests if commonmark is outdated"

This reverts commit 97bee20863.

---------

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

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

* chore: small edits to comments and NEWS item

---------

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

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

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

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

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

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

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

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

* Check-in revdep results

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

* `yarn build` (GitHub Actions)

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

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

---------

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

* Reword busy indication NEWS section

---------

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

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

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

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

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

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

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

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

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

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

* `yarn build` (GitHub Actions)

* Update NEWS.md

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

* Rd docs fixes/improvements

---------

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

* Remove unused global Shiny type

* Add prettier plugin for organizing imports

* Disable eslint indentation rule

* Simplify types

* Add Shiny.connectedPromise and Shiny.sessionInitPromise

* Fix typing issue

* Move prettier plugin to devDependencies

* Rename Shiny class to ShinyClass, and export type

* Remove global Shiny type; use internal imports

* Small code cleanup

* Move initShiny() function into ShinyClass

* Rebuild type files

* Raise error if window.Shiny already exists

* Rename promises

* Add InitStatusPromise class

* `yarn build` (GitHub Actions)

* Update news

* Remove isConnected

* Update yarn.lock

* Rename isInitialized to initializedPromise

* Rebuild shiny.js

* `yarn build` (GitHub Actions)

* Update NEWS

---------

Co-authored-by: wch <wch@users.noreply.github.com>
2024-07-23 22:11:20 -05:00
Randy Zwitch
25c40967da Update issue template to use new forum.posit.co URL (#4038)
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2024-07-23 14:59:17 -04:00
Barret Schloerke
068b232e75 feat(reactlog): Add reactlogAddMark() (#4103) 2024-07-22 11:29:02 -04:00
Garrick Aden-Buie
0b7fda707e chore: Enable return of dependency CSS as Sass files (#4044)
* chore: Enable return of dependency CSS as Sass files

Makes it possible to extract the Sass files prior to compilation for the following CSS:

* shiny
* selectize
* ionrangeslider
* daterange picker

* refactor: Take a more functional approach

* fix: missing selectizeDir

* rename: __SassLayer --> __Sass
2024-06-13 16:08:54 -04:00
Carson Sievert
9fd4ba199e Close #4080: Require bslib 0.6 or higher (#4085) 2024-06-03 09:39:29 -05:00
Dean Attali
43e40c7969 news: fix function name typo (#4076) 2024-06-03 09:13:42 -05:00
Winston Chang
248f19333c Bump cachem dependency. Closes #4032 2024-05-31 21:01:05 -05:00
Garrick Aden-Buie
306c4f847b feat(options): shiny.client_devmode (#4073) 2024-05-30 17:19:41 -04:00
Carson Sievert
e689cdc522 Close #4068: take opacity from last frame of animation (#4069) 2024-05-28 10:33:20 -05:00
Winston Chang
3e0efd8484 Update formatting settings (#4065)
* Format JSON files on save

* Add prettier plugin for organizing imports

* Reorganize imports

* Fix 'routine' build issues

* `yarn build` (GitHub Actions)

---------

Co-authored-by: wch <wch@users.noreply.github.com>
2024-05-24 09:37:28 -05:00
Carson Sievert
4a8400d2a5 Delay longer before dimming opacity when spinners are active (#4062)
* make sure to delay and transition the change in opacity when busy indicators are active

* better approach

* Fix

* Delay a touch longer
2024-05-23 11:52:02 -05:00
Carson Sievert
e432bb0592 Hide the busy pulse when disconnected (#4061) 2024-05-22 12:41:59 -05:00
Carson Sievert
d002734afe Follow up to #4057: remove unused JS files (#4060) 2024-05-21 15:21:27 -05:00
Carson Sievert
54e7377f24 Fade in spinner opacity after initial delay (#4058)
* Fade in spinner opacity after initial delay

* Slightly quicker fade in
2024-05-21 15:06:30 -05:00
Carson Sievert
a49d24108f Close #4056: Gut busy indicator logic to pulse up until 1st idle (#4057) 2024-05-20 17:57:19 -05:00
Garrick Aden-Buie
733a4e8983 fix(pulse): Remove radius from pulse (#4053) 2024-05-15 15:30:18 -04:00
Garrick Aden-Buie
6309a6fca3 tests(busyIndicatorOptions): set seed in the test without a helper (#4052) 2024-05-10 17:23:54 -04:00
Carson Sievert
3d66940402 Add busy indication (#4040)
* First pass at a proper state machine for managing output progress state

* `yarn build` (GitHub Actions)

* Add useBusyIndicators(), spinnerOptions(), and pulseOptions()

* Bring in new spinner defaults

* Use an actual div instead of a pseudo-element since chromium can't be trusted to show them when animated

* Revert "Use an actual div instead of a pseudo-element since chromium can't be trusted to show them when animated"

This reverts commit 6167c1dfd7.

* Embed animation inside svg (to avoid Chromium bug). Consolidate options into a singular busyIndicatorOptions()

* Add to pkgdown reference

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

* `yarn build` (GitHub Actions)

* Bump version

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

* Apply suggestions from code review

Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>

* Update snapshots

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

* Address feedback

* Bring in more spinner type options

* fix use of fs

* Code review

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

* Sync package version (GitHub Actions)

* Update snapshots

* Fix comments

* Make snapshot consistent cross-platform

* Fix namespace issue

* Reduce specificity of position relative

* Skip snapshot on windows; update news

* Whoops

* Scope spinner customizations to parent element by default

* Update snapshots

* Reorder spinner types

* Set a private random seed in tests

* Better id naming

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2024-05-10 14:58:43 -05:00
Carson Sievert
2872c87e32 Allow for progress binding to move from Idle to Invalidated status (#4047)
* Close #4046: Allow for progress binding to move from Idle to Invalidated status

* `yarn build` (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2024-05-07 17:46:46 -05:00
Carson Sievert
ecb591f2e1 Disable downloadLink()/downloadButton() until their href is ready (#4041)
* Close #3606: disable downloadLink()/downloadButton() until their href has been populated

* Update news
2024-05-06 09:36:59 -05:00
Carson Sievert
8e37d45948 Improve/fix output progress reporting (#4039)
* First pass at a proper state machine for managing output progress state

* `yarn build` (GitHub Actions)

* Improved naming

* Include ascii version of diagram (as well as the source file

* Update srcts/src/shiny/outputProgress.ts

Co-authored-by: Joe Cheng <joe@rstudio.com>

* `yarn build` (GitHub Actions)

* Only call showProgress for outputs that have actually changed their recalculating status

* Add some more commentary

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
Co-authored-by: Joe Cheng <joe@rstudio.com>
2024-05-02 18:01:29 -05:00
Winston Chang
c11f120bb9 Change eslint-disable comments to eslint-disable-next-line (#4037)
* Change eslint-disable comments to eslint-disable-next-line

* Allow floating promise

* `yarn build` (GitHub Actions)

---------

Co-authored-by: wch <wch@users.noreply.github.com>
2024-04-30 13:19:48 -05:00
Winston Chang
950c63049b Check that $socket exists before sending message (#4035)
* Check that socket exists before sending message

* `yarn build` (GitHub Actions)

---------

Co-authored-by: wch <wch@users.noreply.github.com>
2024-04-26 15:21:35 -05:00
Carson Sievert
3edf9bfad8 Fix opacity dimming on recalculating uiOutput() (#4028)
* Close #4027: Fix opacity dimming on recalculating uiOutput(). Also, only apply display:content when there are child elements

* Update inst/www/shared/shiny_scss/shiny.bootstrap5.scss

* Add news item
2024-04-10 13:16:41 -05:00
Carson Sievert
420a2c054c Start new version (#4023)
* Start new version

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2024-04-03 10:03:31 -05:00
Carson Sievert
5e566a057d Start v1.8.1.1 release candidate (#4020)
* Start v1.8.1.1 release candidate

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

* Remove alpha from npm version

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2024-04-03 09:26:01 -05:00
Carson Sievert
edd1db78e3 Warn instead of error when duplicate binding IDs are found in non-devmode (#4019)
* Close #4016. Warn instead of error when duplicate binding IDs are found in non-devmode

* Get rid of unreachable ShinyClientError()

* `yarn build` (GitHub Actions)

* Update srcts/src/shiny/bind.ts

Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>

* `yarn build` (GitHub Actions)

* Move logic to where error gets thrown not constructed

* `yarn build` (GitHub Actions)

* Update NEWS

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2024-03-29 13:51:46 -05:00
Joe Cheng
47526a769a ExtendedTask should not be cloneable (#4015) 2024-03-27 19:06:21 -05:00
Carson Sievert
0474eeeead Start new version (#4014)
* Start new version

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2024-03-27 10:10:45 -05:00
Carson Sievert
e8cdc78f0f v1.8.1 release candidate (#4008)
* Start v1.8.1 release candidate

* Update urls and yarn build

* Sync package version (GitHub Actions)

* Check-in revdep results

* Groom news

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2024-03-27 10:01:08 -05:00
Carson Sievert
7742b652ba Fix and simplify reactlog's version check approach. (#4012)
* Close #4011. Fix and simplify reactlog's version check approach.

* Better variable name

* Use test_path() for consistent path location

* Just use packageDescription()

* Update tests/testthat/test-reactlog.R

* Update tests/testthat/test-reactlog.R

* Update DESCRIPTION
2024-03-22 17:39:52 -05:00
Carson Sievert
7ed68ed927 Revert changes to shinyDeprecated() & update renderDataTables() tests to always use shiny's datatables implementation (#4010)
* Revert "feat: Deprecate with warnings, expect `renderDataTable()` to be deprecated (#4007)"

This reverts commit a8c6065b9f.

* Use shiny's datatables implementation when testing
2024-03-22 17:35:15 -05:00
Garrick Aden-Buie
ac06350e08 chore: restyle new examples-shiny apps (#4004)
* chore: restyle examples-shiny

* chore: restore select newlines

* More consistent approach to whitespace

---------

Co-authored-by: Carson <cpsievert1@gmail.com>
2024-03-21 17:43:30 -05:00
Garrick Aden-Buie
43698f0860 feat: Run examples from any package (#4005)
* chore: `inst/shiny` -> `inst/examples-shiny`

* feat(runExamples): Find examples in any package

* refactor: code style

* refactor: small code style changes

* docs: fix runApp typo

* chore: include package name in valid examples message

* chore(runExample): check that `package` is installed

* chore: use braced package name

* Update news

---------

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2024-03-21 12:07:57 -05:00
avoidaway
c73e1a21b8 chore: remove repetitive words (#3999)
Signed-off-by: avoidaway <cmoman@126.com>
2024-03-21 11:30:24 -05:00
Garrick Aden-Buie
d855468398 chore: inst/shiny -> inst/examples-shiny (#4003) 2024-03-21 10:58:50 -05:00
Garrick Aden-Buie
b8efd88448 docs(MockShinySession): document unhandled error param (#4006) 2024-03-21 10:58:27 -05:00
Garrick Aden-Buie
a8c6065b9f feat: Deprecate with warnings, expect renderDataTable() to be deprecated (#4007)
* feat: Deprecate with a warning

Use `rlang::warn()` instead of `rlang::inform()` and use the lifecycle warning class so that lifecycle infrastructure works

* tests: expect that renderDataTable is deprecated

* tests: invert warning and error expectation
2024-03-21 10:57:50 -05:00
Carson Sievert
12a8b228d9 Follow up to #3998: correct version requirement in message. (#4002) 2024-03-21 09:12:01 -05:00
Sara Altman
47fb562151 Update examples to use bslib (#3963)
* update 01_hello

* Updates 06_tabsets example to use bslib

* Pre-emptively adds legacy versions of all of the original examples

* Switches tabset example to use navset_card_underline()

* Updates example 2

* Updates 03_reactivity

* Updates 04_mpg

* Updates 05_sliders

* Updates 07_widgets

* Deletes 08_html_legacy. Original example does not need updated.

* Updates 09_upload

* Updates 10_download

* Updates 11_timer

* Keep inst/example unchanged; add new examples under inst/shiny and update runExamples() to use that directory by default

* Update news

* Fix some code formatting issues

* Update NEWS.md

* Don't default to showcase mode

---------

Co-authored-by: Garrett Grolemund <grolemund@gmail.com>
Co-authored-by: Carson <cpsievert1@gmail.com>
2024-03-20 16:04:58 -05:00
Carson Sievert
f0059b71e5 Use bs-danger instead of bs-danger-text-emphasis for text color on error messages (#4001) 2024-03-20 12:18:05 -05:00
Carson Sievert
89aaa977e8 Update datatables.js from 1.10.5 to 1.10.22 and deprecate renderDataTable()/dataTableOutput() (#3998)
* Update datatables.js from 1.10.5 to 1.10.22. Deprecate renderDataTable()/DTOutput()

* Update news; go back to old types version

* Clean up

* Update NEWS.md

* Update NEWS.md
2024-03-19 14:50:15 -05:00
Kirill Müller
ae308e03ad Avoid R package warning in loadSupport() if autoload is disabled (#3513)
* Avoid bogus warning

* refactor: source globals first, then deal with helpers

* refactor: Factor out `warn_if_app_dir_is_package()` for readability

* chore: simplify diff

* docs(news): Add news item

* Hint to help file in message.

Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>

---------

Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2024-03-15 15:50:35 -05:00
Garrick Aden-Buie
c1a1542cfe fix(updateSelectizeInput): Clear current value before update if selected and sever = TRUE (#3967)
* fix(updateSelectizeInput): Clear current value before update if `selected` and `server = TRUE`

Fixes #3966

* chore: yarn build

* `yarn build` (GitHub Actions)

* docs(news): Add news item

* chore: Add comments for future selves

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

* `yarn build` (GitHub Actions)

---------

Co-authored-by: gadenbuie <gadenbuie@users.noreply.github.com>
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2024-03-14 09:56:06 -05:00
Garrick Aden-Buie
3c4a908773 feat: Add onUnhandledError() (#3993)
* feat: Add `onUnhandledError()`

* docs(NEWS): Update previous news item

* docs(onUnhandledError): Add example

* docs(MockShinySession): Fix unhandledErrorCallbacks private field docs

* feat: Handle non-fatal unhandled errors, too

* docs(onUnhandledError): Add more detailed documentation

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

* docs: Small edits
2024-03-14 10:21:59 -04:00
Garrick Aden-Buie
e2b7f91138 feat: Add shiny.error.unhandled error handler (#3989)
* feat(shiny.error.unhandled): Allow users to provide an unhandled error handler

* Extract `shinyUserErrorUnhandled()` to use in MockSession too

* tests(shiny.error.unhandled): Test that unhandled errors are handled safely

* docs: Clarify that session still ends with an unhandled error

* docs: Add news item
2024-03-08 13:36:36 -06:00
Garrick Aden-Buie
c73978cdd5 docs: update roxygen2 (#3988)
* fix: `@docType "package"` is deprecated

* fix: S3 methods need `@export` or `@exportS3method` tag.

* chore: devtools::document()
2024-03-08 09:15:37 -06:00
Andreas Deininger
6760c31818 Documentation: fixing typos (#3932)
* Documentation: fixing typos

* Commit changes after generation of package docs
2024-02-02 16:53:48 -06:00
olivroy
781ceaaa5c Remove ellipsis dependency (#3959)
* Remove ellipsis dependency

* Use the above `@importfrom rlang`

* move usethis namespace
2024-02-02 16:52:39 -06:00
Fenno Vermeij
fff283648b fix(downloadButton): Return tag directly (#2672)
* Fix `downloadButton()` not rendering in rmarkdown documents

---------

Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2024-01-30 17:31:40 -05:00
Winston Chang
f71f1256b8 Fix duplicate ID check logic (#3978)
* Fix duplicate ID logic

* Build shiny.js
2024-01-24 14:42:20 -06:00
Nick Strayer
f26b1335d8 Dev mode aware client and duplicate input/output ID handling updates (#3956)
* Add field with devmode status to the initial config object sent over on connection

* Add indicator of "devmode" status to the client via an injected script tag on load. This is modeled after what is done for showcase mode.

* Add logic to flag all duplicated IDs when in devmode

* Only show error console in devmode.

* Remove left-over devmode status in code

* `yarn build` (GitHub Actions)

* Build shiny.js

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

* `yarn build` (GitHub Actions)

---------

Co-authored-by: nstrayer <nstrayer@users.noreply.github.com>
Co-authored-by: Winston Chang <winston@posit.co>
Co-authored-by: wch <wch@users.noreply.github.com>
2024-01-24 12:37:27 -06:00
Nick Strayer
370ba1f288 Make error console sizing constant across base and bslib apps (#3947)
* Add more encapsulated sizes using css variables instead of `rem` units so console is consistantly sized across apps that set the body font size.

* `yarn build` (GitHub Actions)

* `yarn build` (GitHub Actions)

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

* `yarn build` (GitHub Actions)

---------

Co-authored-by: nstrayer <nstrayer@users.noreply.github.com>
Co-authored-by: jcheng5 <jcheng5@users.noreply.github.com>
Co-authored-by: Winston Chang <winston@posit.co>
Co-authored-by: wch <wch@users.noreply.github.com>
2024-01-22 14:17:10 -06:00
Joe Cheng
54988c17c8 Merge pull request #3958 from rstudio/extended-task
Add ExtendedTask R6 class
2024-01-11 09:11:58 -08:00
Joe Cheng
65fe23fa02 Don't take reactive dependency on rv_status() during invoke 2024-01-11 08:59:46 -08:00
Joe Cheng
b22b06e3d2 Add NEWS 2024-01-10 19:00:11 -08:00
Joe Cheng
3677f4e1c6 Add unit tests for maskReactiveContext 2024-01-10 19:00:11 -08:00
Joe Cheng
d6eb0493b3 Code review feedback 2024-01-10 19:00:11 -08:00
Joe Cheng
4e13cdb365 Realized we no longer care about the bslib version
Now that bind_task_to_button is gone to bslib, there's no specific
code in Shiny that cares about task buttons
2024-01-10 19:00:11 -08:00
Joe Cheng
4e3710cdaa Use correct remote 2024-01-10 19:00:11 -08:00
Joe Cheng
5feedaf4c8 Add Remotes for bslib 2024-01-10 19:00:11 -08:00
Joe Cheng
ce29695e44 Remove bind_button_to_task (moved to bslib::bind_task_button) 2024-01-10 19:00:11 -08:00
Joe Cheng
f0f06a2c34 Update bind_button_to_task to use newest task button API 2024-01-10 19:00:11 -08:00
Joe Cheng
860a3fef86 Rebuild 2024-01-10 19:00:11 -08:00
Joe Cheng
6afadade5d Add bind_button_to_task feature 2024-01-10 19:00:11 -08:00
jcheng5
c1bda7fb7b yarn build (GitHub Actions) 2024-01-10 19:00:11 -08:00
jcheng5
509c165ee8 devtools::document() (GitHub Actions) 2024-01-10 19:00:11 -08:00
Joe Cheng
54e0ef7598 Add ExtendedTask to pkgdown.yml 2024-01-10 19:00:11 -08:00
Joe Cheng
03f2d5f014 Add ExtendedTask R6 class 2024-01-10 19:00:11 -08:00
Garrick Aden-Buie
122c1e74cd refactor: pass-through containers in BS5 only (#3960) 2023-12-20 19:15:09 -05:00
Garrick Aden-Buie
d29f4cdf21 fix(ui-containers): Use display: contents (#3957) 2023-12-19 22:27:50 -05:00
Joe Cheng
300fb217d1 Merge pull request #3954 from rstudio/persistent-progress
Allow outputs to stay in progress mode after flush
2023-12-12 12:23:25 -08:00
Joe Cheng
33dc41c4bd Add disabled argument to actionButton and updateActionButton 2023-12-11 17:04:09 -08:00
Joe Cheng
4b6e257dfc Don't add progressKeys more than once
Doesn't matter much but this is closer to the old behavior
2023-12-07 09:22:01 -08:00
Joe Cheng
1f23f37f89 Allow outputs to stay in progress mode after flush
Adds a req(FALSE, cancelOutput="progress") which behaves similarly to
cancelOutput=TRUE, but also keeps the output in .recalculating state
even across flush cycles. This is called "persistent progress" and an
output can leave this state when it is invalidated again and doesn't
call req(FALSE, cancelOutput="progress") during that flush cycle.

This will be useful for implementing long-running tasks that don't
hold up the flush cycle, leaving sessions responsive to do other
tasks.
2023-12-06 09:30:58 -08:00
Garrick Aden-Buie
59b1c46485 fix: Allow bindInputs() to no-op when attempting to bind currently bound inputs (#3946)
* fix: Do not re-bind previously bound inputs

* refactor: Add binding to the registry after binding happens

* fix: Spelling of `bindingsRegistry`

* chore: yarn build

* `yarn build` (GitHub Actions)

* fix: spelling

* feat: isRegistered can check if bound to input or output

* fix: Do not throw for shared input/output IDs

`input$caption` and `output$caption` may not be the best idea for several reasons, but it was previously allowed

Fixes #3943

* fix: check element directly to know whether it a bound input

* chore: yarn build

* fix: test `.shiny-bound-input` instead of data prop

* refactor: Remove `bindingsRegistry.isRegistered()` method

* refactor: Use a map for duplicateIds again

* refactor: Add `BindingTypes` type and use `bindingType` everywhere

* refactor: More concise duplicateIds typing

Co-authored-by: Nick Strayer <nick.strayer@rstudio.com>

* refactor: count by forEach + incrementing

Co-authored-by: Nick Strayer <nick.strayer@rstudio.com>

* `yarn build` (GitHub Actions)

* thanks, vscode

* docs: rewrite checkValidity() jsdoc to capture current state of things

* chore: yarn build

* docs: slight rewording

---------

Co-authored-by: gadenbuie <gadenbuie@users.noreply.github.com>
Co-authored-by: Nick Strayer <nick.strayer@rstudio.com>
2023-11-30 15:46:28 -06:00
Garrick Aden-Buie
01705c1299 fix(shiny.scss): Constrain notification panel to max-width: 100% (#3949) 2023-11-30 16:11:22 -05:00
Carson Sievert
18955a2abf Update tabPanel() snapshot tests in anticipation of bslib release (#3936)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-11-29 15:38:48 -06:00
Nick Strayer
dbbe7f9679 Client error console and duplicate input/output binding errors (#3931)
Co-authored-by: nstrayer <nstrayer@users.noreply.github.com>
Co-authored-by: Winston Chang <winston@stdout.org>
2023-11-27 12:34:13 -06:00
Carson Sievert
61a51a869f Run yarn build (#3942)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-11-20 13:32:32 -06:00
Carson
298822fc44 Start new version 2023-11-20 12:57:08 -06:00
Carson Sievert
283c71e772 v1.8.0 release candidate (#3928) 2023-11-20 12:55:11 -06:00
Carson Sievert
b1297395a9 Don't treat input/output as draggable inside of absolutePanel(draggable = T) (#3937) 2023-11-01 10:42:19 -05:00
Carson Sievert
b850cd2509 Change selectize.js selectOnTab option back to false (#3935)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-10-31 16:45:35 -05:00
Carson Sievert
56878ebbaa Revert Shiny.renderContent() back to sync instead of async (#3929)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-10-30 12:59:22 -05:00
Winston Chang
6a09fda08e Allow InputBinding.receiveMessage to be async (#3930)
Co-authored-by: wch <wch@users.noreply.github.com>
2023-10-30 11:26:04 -05:00
Carson Sievert
77bc4e9ec7 Update selectInput's binding to use selectize.js' getValue() method when relevant (#3926)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-10-26 18:21:40 -05:00
Carson Sievert
a1b9fda809 Prevent .selectize() updates from destroying .data() and event listeners (#3923)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-10-26 17:42:06 -05:00
Garrick Aden-Buie
97a12ec601 fix(shiny.scss): Ensure $body-emphasis-color is defined for all BS5 (#3924) 2023-10-26 14:36:49 -05:00
Winston Chang
81bdde64c4 Convert bindAll to an async function (#3904)
Co-authored-by: Carson <cpsievert1@gmail.com>
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-10-24 16:01:47 -05:00
Garrick Aden-Buie
c4ef42337b feat: Improve notification styles (#3913)
Co-authored-by: gadenbuie <gadenbuie@users.noreply.github.com>
Co-authored-by: Carson <cpsievert1@gmail.com>
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-10-20 10:55:23 -05:00
Carson Sievert
ce78d0dcf1 Fix selectize event namespacing issues (#3919)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-10-20 10:21:29 -05:00
Carson Sievert
7069064dd6 Add non-minified selectize.js source; and respect shiny.minified option (#3918) 2023-10-19 18:38:30 -05:00
Carson Sievert
a0a83d5fe3 Merge pull request #3914 from rstudio/chore/shiny-sass-bs-3-4-5 2023-10-19 16:40:59 -05:00
Garrick Aden-Buie
8fbc4ad4c1 Merged origin/main into chore/shiny-sass-bs-3-4-5 2023-10-19 17:31:30 -04:00
Garrick Aden-Buie
5346a00373 docs: Add news item 2023-10-19 17:30:59 -04:00
Garrick Aden-Buie
2dc69aea37 chore(shiny-sass): Remove comments 2023-10-19 17:27:08 -04:00
Joe Cheng
be6f6716bf Merge pull request #3915 from rstudio/autoreload-indication
Soften visually jarring greyout when autoreloading
2023-10-18 12:23:47 -07:00
Joe Cheng
7f59f93692 Only show the lighter curtain if autoreload is on
I added this cause I'm slightly worried about other server environments
sending the same 1012 close code.
2023-10-16 17:58:02 -07:00
Joe Cheng
798b336df6 Make autoreload survive laptop suspend
Also simplify the implementation for softening the grey curtain when
autoreload is in progress (only applies to Shiny for Python)
2023-10-16 17:39:55 -07:00
Joe Cheng
bef6b4bfd9 Soften visually jarring greyout when autoreloading
This change detects when Shiny is autoreloading, and instead of
showing the typical disconnection treatment (dark grey), it
initially shows nothing and then quickly fades to very light
grey.

The goal is for instant autoreloads to feel seamless, but longer
autoreloads to make it clear when the autoreload has completed,
and still not feel as jarring as a full disconnect.
2023-10-16 17:39:55 -07:00
Carson Sievert
80ab088e2d Merge pull request #3910 from rstudio/rc-v1.7.5.1 2023-10-16 12:22:58 -05:00
Carson
481a692b07 Merge branch 'main' into rc-v1.7.5.1 2023-10-16 10:01:18 -05:00
Garrick Aden-Buie
8ae936ba01 feat(shiny-scss): Create variable defaults specific to the Bootstrap version 2023-10-11 11:59:08 -04:00
Garrick Aden-Buie
6dc377842f Revert shiny bootstrap sass back to pre-BS 5.3
Reverts back to commit f540679513
2023-10-11 11:06:11 -04:00
Carson
837307fe8c Remove alpha from the npm version 2023-10-06 09:52:39 -05:00
Carson
dfe359c1b6 shiny v1.7.5.1 2023-10-05 18:42:03 -05:00
Carson Sievert
b8923e9497 Address r-devel's change in is.atomic(NULL) behavior (#3908) 2023-10-03 10:54:55 -05:00
Winston Chang
9ebcbf8a2d wrapFunctionLabel: handle case when name is >10000 bytes (#3903)
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2023-09-27 10:38:28 -05:00
Carson Sievert
a6fc6bf8cb Update to the development version of selectize.js (#3897)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-09-14 19:02:31 -05:00
Carson Sievert
eddc3047d4 Fix regression in updateSelectizeInput() (#3890)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-09-08 16:48:48 -05:00
Carson Sievert
6db17d4f67 Ensure Sass variables are defined for Bootstrap 3 Sass (#3892) 2023-09-08 10:39:25 -05:00
Winston Chang
d21f9493fb Update fileInput help page (#3886)
Co-authored-by: wch <wch@users.noreply.github.com>
2023-09-01 15:27:28 -05:00
Carson Sievert
e87f942e89 Update Shiny's CSS to respect BS5's dark color mode (#3882)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-08-28 15:11:01 -05:00
Carson Sievert
1eb9ed7345 Upgrade to selectize.js 0.15.2 (#3875)
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-08-28 14:59:43 -05:00
Carson Sievert
9d923d079a fileInput()'s button shouldn't have top-right or bottom-right border-radius (#3879) 2023-08-24 09:07:11 -05:00
Carson Sievert
b054e45402 Update staticimports (#3872) 2023-08-15 11:40:19 -05:00
Carson Sievert
8b1d30aefe use_dev_version() (#3871)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-08-14 12:53:12 -05:00
Carson
ab87a0708d use_dev_version() 2023-08-14 09:56:22 -05:00
Carson Sievert
0b97ee1ecc 1.7.5 release candidate (#3867)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-08-14 09:49:46 -05:00
Garrick Aden-Buie
68546c319e Increase touch target area of slider input (#3859) 2023-08-07 15:14:35 -04:00
Garrick Aden-Buie
69188fef22 chore(desc): Add GitHub repo to URL (#3864) 2023-08-03 13:20:33 -04:00
Garrick Aden-Buie
6be6dfbfeb Finer-grained control over shiny-provided input bindings (#3861)
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
Co-authored-by: Joe Cheng <joe@rstudio.com>
Co-authored-by: gadenbuie <gadenbuie@users.noreply.github.com>
2023-07-31 10:41:27 -05:00
Garrick Aden-Buie
6fc06281bd docs(navbarPage): Be less specific about collapsible breakpoint (#3853) 2023-07-07 10:42:39 -04:00
Barret Schloerke
f724128d41 Merge pull request #3850 from rstudio/rc-v1.7.4.1 2023-07-07 09:56:25 -04:00
Barret Schloerke
518ef0f9f8 Merge branch 'main' into rc-v1.7.4.1
* main: (55 commits)
  Catch errors receiving individual input messages (#3843)
  Clarify what's ignored by ignoreNULL (#3827)
  Remove tests/testthat/apps/ (#3841)
  Correctly set width on `textarea` input element (#3838)
  Update selectize.js documentation URLs (#3836)
  test(json digits): Try unsetting option in test to get default behavior (#3837)
  Rebuild docs
  Update README.md
  fix typo in the docs for bindCache (#3825)
  Allow for `shiny:::toJSON()` to respect if `digits` has class `AsIs` to represent `use_signif=` (#3819)
  More complete `downloadButton()` example
  Rename actionQueue to taskQueue, add more context to the NEWS item (#3801)
  Remove unneeded packages from package.json
  Rebuild docs
  Rebuild shiny.js
  Update @types/node
  Rebuild yarn.lock
  Fix brush resetting behavior. Closes #3785
  Bump fastmap dependency to 1.1.1
  Sync package version (GitHub Actions)
  ...
2023-07-07 09:39:01 -04:00
Winston Chang
f5b395485e shiny v1.7.4.1
Remove use of as.numeric_version()
Closes #3849
2023-07-07 09:33:14 -04:00
Garrick Aden-Buie
31aca7aa70 Catch errors receiving individual input messages (#3843) 2023-06-30 17:10:17 -04:00
Garrick Aden-Buie
b38a630224 Clarify what's ignored by ignoreNULL (#3827)
Co-authored-by: gadenbuie <gadenbuie@users.noreply.github.com>
2023-06-30 10:44:43 -04:00
Winston Chang
1b7709411b Remove tests/testthat/apps/ (#3841) 2023-06-16 16:55:14 -05:00
Garrick Aden-Buie
2b48aa0d91 Correctly set width on textarea input element (#3838)
Co-authored-by: gadenbuie <gadenbuie@users.noreply.github.com>
2023-06-12 15:57:49 -04:00
Viviane Girardin
6fdf23752e Update selectize.js documentation URLs (#3836)
Co-authored-by: Neal Richardson <neal.p.richardson@gmail.com>
Co-authored-by: Winston Chang <winston@stdout.org>
2023-06-09 15:43:21 -05:00
Barret Schloerke
8542f5d017 test(json digits): Try unsetting option in test to get default behavior (#3837) 2023-06-09 15:30:02 -05:00
Winston Chang
e7b830755a Rebuild docs 2023-06-01 15:44:18 -05:00
Gordon Shotwell
23c7b0683a Update README.md
Add Discord link
2023-06-01 15:43:20 -05:00
Tom Jemmett
5805895581 fix typo in the docs for bindCache (#3825) 2023-05-23 08:52:42 -05:00
Barret Schloerke
90539bff25 Allow for shiny:::toJSON() to respect if digits has class AsIs to represent use_signif= (#3819) 2023-05-16 16:14:03 -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
Winston Chang
98f17e0cd2 Disable eslint rules only within scope 2022-10-03 11:41:28 -05:00
Winston Chang
9b2c04f298 Remove redundant setting 2022-09-30 19:57:03 -05:00
Winston Chang
ed4a97154d Remove makeBlob
Blob has long been available on all major browsers, so makeBlob is no longer needed.
2022-09-30 16:29:38 -05:00
Winston Chang
9dcd62f944 Update eslint 2022-09-30 16:17:19 -05:00
Winston Chang
213c645524 Upgrade esbuild and typescript 2022-09-30 15:59:24 -05:00
Winston Chang
f1c0ac2b30 Upgrade to yarn 3.2.3 2022-09-30 15:57:51 -05:00
Barret Schloerke
16c6d55f60 Enable TypeScript strict mode (#3644) 2022-09-29 16:03:05 -04:00
Hedley
6e40a3dd39 Update jQuery-UI to 1.13.2 (#3697) 2022-09-21 10:34:51 -04:00
Joe Cheng
04ad1453c1 Merge pull request #3694 from rstudio/rook-doc-link
Add link to Rook spec from docs
2022-09-07 17:38:36 -07:00
jcheng5
80eeff68ab Sync package version (GitHub Actions) 2022-09-07 14:27:51 -07:00
jcheng5
6128a3ab65 yarn build (GitHub Actions) 2022-09-07 20:43:50 +00:00
Joe Cheng
5f25537079 Add link to Rook spec from docs 2022-09-07 13:37:37 -07:00
Winston Chang
c21ba0baca Bump version to 1.7.2.9000 2022-07-19 09:18:16 -05:00
Winston Chang
ebf786c2eb Merge tag 'v1.7.2'
Shiny 1.7.2 on CRAN
2022-07-19 09:16:45 -05:00
wch
b39ffafea9 devtools::document() (GitHub Actions) 2022-07-19 00:16:11 +00:00
Winston Chang
4441945a68 Use inherits() instead of if(class(x)==y) 2022-07-18 19:11:24 -05:00
Winston Chang
cd95e058e6 Remove URL because CRAN doesn't like it 2022-07-18 19:11:24 -05:00
Winston Chang
a0144d77ef Remove broken link 2022-07-18 19:11:24 -05:00
Carson
64cec08a74 Check in most recent revdep results 2022-07-08 11:53:50 -05:00
Winston Chang
7a77b55e6a Merge branch 'main' into rc-v1.7.2 2022-07-05 20:08:10 -05:00
Winston Chang
54e5a6b43c Merge branch 'dvg-p4-fix-throttle' 2022-07-05 20:03:22 -05:00
Winston Chang
9653cc2893 Rebuild shiny.js 2022-07-05 20:01:22 -05:00
Winston Chang
47dc5b4116 Code and comment cleanup 2022-07-05 19:37:44 -05:00
dvg-p4
9db9ef527a Fixed check for isPending and rebuilt javascript 2022-07-04 10:21:22 -04:00
dvg-p4
9285a1f7fc Update srcts/src/time/throttle.ts
Based on suggestion

Co-authored-by: Winston Chang <winston@stdout.org>
2022-07-01 19:02:26 -04:00
dvg-p4
d22eb1524a Updated NEWS.md 2022-07-01 17:15:09 -04:00
dvg-p4
5e3971c776 Fixed major bug in throttle.ts 2022-07-01 16:58:41 -04:00
Carson
dbe4896102 Merge branch 'main' into rc-v1.7.2 2022-06-27 12:12:55 -05:00
Joe Cheng
ff5ef52dd5 Fix #3250 (#3602)
* Fix #3250

pruneStackTrace was interacting badly with dplyr errors. I'm still
not sure what causes these new cases, but the new behavior seems to
be much better, with no downside that I can think of.

* Fix existing unit tests

* Update news

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2022-06-27 12:05:28 -05:00
Joe Cheng
634b1c7c3c Don't kill the session when a debounced/throttled reactive expr errors (#3624)
* Don't kill the session when a debounced/throttled reactive expr errors

Fixes #3581

* Update NEWS with PR number

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2022-06-27 10:57:10 -05:00
Carson
1c9f8940a9 Merge branch 'main' into rc-v1.7.2 2022-06-24 17:53:49 -05:00
Carson Sievert
d4527cdc28 Use ragg::agg_png over Cairo::CairoPNG if available (#3654)
* Close #3626: use ragg::agg_png over Cairo::CairoPNG if available

* Update documentation
2022-06-24 17:50:58 -05:00
cpsievert
514206850a Sync package version (GitHub Actions) 2022-06-14 15:43:58 +00:00
cpsievert
809bc8c6de yarn build (GitHub Actions) 2022-06-14 15:42:16 +00:00
cpsievert
0d720616f3 devtools::document() (GitHub Actions) 2022-06-14 15:39:35 +00:00
Carson
0c325d422f Groom NEWS.md 2022-06-14 10:35:36 -05:00
Carson
d368aa72c3 Update URLs 2022-06-14 10:35:36 -05:00
Carson
27e1348dcb Start v1.7.2 release candidate 2022-06-14 10:35:36 -05:00
Carson Sievert
474f14003b Follow up to #3385: warn instead of message; update unit tests to reflect some parameters can now succeed when others fail (#3652) 2022-06-14 10:34:20 -05:00
Carson Sievert
8a5da25545 Fix/update news (#3651) 2022-06-14 09:18:51 -05:00
Barret Schloerke
540d68ed9f Update the _inputs_ and _values_ regular expr to support a trailing = (#3648) 2022-06-10 11:39:12 -04:00
Khaled Al-Shamaa
1ad49b153c Enable fileInput to set the capture attribute (#3481)
Co-authored-by: Barret Schloerke <barret@rstudio.com>
Co-authored-by: Barret Schloerke <schloerke@gmail.com>
Co-authored-by: Winston Chang <winston@stdout.org>
2022-06-10 10:30:34 -05:00
Winston Chang
15885cbb5f Update NEWS 2022-06-10 10:07:00 -05:00
Dean Attali
b6979d135c fix bookmarking bug #2297: don't break all bookmarking system if some URL params don't parse correctly (#3385)
Co-authored-by: Barret Schloerke <barret@rstudio.com>
2022-06-10 10:04:47 -05:00
Winston Chang
d4b19820a4 Update NEWS 2022-06-10 10:02:30 -05:00
Dieter Menne
8d529095a7 Corrected for stricter length checking in R 4.2.0 (#3625)
* Corrected for stricter length checking in R 4.2.0

* Update R/bootstrap-layout.R

Fine! I had thought of that case, but could not find that elegant solution

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

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2022-06-10 09:59:14 -05:00
Winston Chang
77f9052ab5 Make mathjax configurable (#3650)
Co-authored-by: Neutron3529 <qweytr_1@163.com>
Co-authored-by: Joe Cheng <joe@rstudio.com>
2022-06-10 09:57:02 -05:00
Ryan Barnard
9fcc1fe8ad Fixed automatic guessing of Content-Type in downloadHandler (#3393)
* Set default downloadHandler contentType to NULL.

The change from %OR% to %||% broke automatic guessing of content type
since `NA %||% ...` evaluates to `NA`. Setting the default contentType
to NULL restores the previous behavior of automatically setting the
content type based on the file extension.

* Updated NEWS.md: downloadHandler contentType fix.

* Update NEWS.md

Co-authored-by: Winston Chang <winston@stdout.org>
Co-authored-by: Barret Schloerke <barret@rstudio.com>
2022-06-10 09:39:00 -05:00
Barret Schloerke
5d30b55372 Spelling defintion -> definition (#3649) 2022-06-09 16:52:27 -04:00
Carson Sievert
78d77ce373 insertUI() now supports execution of <script> (#3630) 2022-05-10 11:43:21 -05:00
Joe Cheng
2cae04186b Merge pull request #3628 from rstudio/joe/feature/autoreload-custom-url
Add ability for autoreload ws to be at a custom URL
2022-05-04 16:35:39 -07:00
Joe Cheng
59bddea1e9 Use external, not internal, sourcemaps for extras 2022-05-04 16:23:48 -07:00
Joe Cheng
d6bd3d9f9b Add ability for autoreload ws to be at a custom URL 2022-05-04 09:05:53 -07:00
Joe Cheng
8eb7b056f2 devmode should activate autoreload (#3620)
devmode should activate autoreload

It said it didn't, but until this commit, it appeared not to
2022-04-27 13:51:02 -07:00
Barret Schloerke
40ae9a903e Spelling (#3618) 2022-04-23 12:25:11 -04:00
Barret Schloerke
5b6c80d4b2 Update shinyAppTemplate() content to use {shinytest2} (#3599) 2022-04-22 16:10:11 -04:00
Dean Attali
fd7518018c Update internal docs: reexports.yml -> reexports.json (#3522) 2022-03-11 11:59:35 -05:00
Kathryn Doering
5c03326a8c Use HEAD for ref instead of master in runGitHub() (#3564)
Co-authored-by: Kathryn Doering <kathryn.doering@noaa.gov>
2022-02-14 15:53:33 -05:00
Barret Schloerke
2c82ee0235 Bump dev version (#3588) 2022-02-14 15:24:47 -05:00
Barret Schloerke
ac84be956a Opt-in to C collate order in test snapshots (#3515) 2022-02-14 14:12:25 -05:00
Winston Chang
0fb154cc1e Trigger input event even when there is no input binding (#3584)
Co-authored-by: Winston Chang <winston@stdout.org>
Co-authored-by: Barret Schloerke <schloerke@gmail.com>
2022-02-14 13:45:59 -05:00
Carson Sievert
837e8d33f6 Update stack trace test expectations (#3550) 2022-02-14 13:14:10 -05:00
Winston Chang
3365bfc395 Merge pull request #3583 from ismirsehregal/main 2022-02-09 17:27:18 -06:00
ismirsehregal
135fe21278 Update runapp.R
Fixed typo
2022-02-09 15:11:34 +01:00
Carson Sievert
fc7e237000 Pass args from knit_print.shiny.render.function() down to it's use of the knit_print() generic (#3569)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
Co-authored-by: Barret Schloerke <barret@rstudio.com>
Co-authored-by: schloerke <schloerke@users.noreply.github.com>
2022-01-11 12:21:18 -06:00
Winston Chang
de8134742d Update NEWS 2022-01-11 10:41:19 -06:00
Winston Chang
f814034835 Merge pull request #3570 from romainfrancois/shinyActionButtonValue 2022-01-11 10:38:28 -06:00
Romain Francois
6d9fad29f3 put the extension class on the left.
from: https://github.com/tidyverse/dplyr/issues/6154
2022-01-11 12:49:48 +01:00
Lionel Henry
313ae9044d Handle chained errors (#3567)
Closes tidyverse/dplyr#5552
Part of #3566
2022-01-10 18:33:14 -08:00
Barret Schloerke
9389160af0 Cache the reactlog version found. Remove mustWork argument to system_file(). (#3554) 2021-12-08 15:30:37 -05:00
Barret Schloerke
6a7ffeff68 master -> main; use shiny-workflows (#3535) 2021-11-19 17:50:57 -05:00
Winston Chang
bc6ff57cb7 Remove old unused JS files (#3547) 2021-11-17 22:01:19 -06:00
Carson Sievert
b52b9e4520 Some UI-related speed improvements (#3541) 2021-11-17 16:25:27 -06:00
Winston Chang
fb71ab6146 Editor configuration improvements 2021-11-16 15:38:21 -06:00
Carson Sievert
d8c7a634ff Fix failing tabPanel() test broken by testthat 3.1.0 (#3543) 2021-11-05 16:45:16 -05:00
Winston Chang
396dd2632e Merge pull request #3537 from rstudio/htmldep 2021-11-05 15:56:09 -05:00
Winston Chang
c11875a5f0 Fix appending to list 2021-11-05 15:52:14 -05:00
Winston Chang
2e599faf1f Fix comment 2021-11-05 15:47:23 -05:00
Winston Chang
a5a8385420 Use all_files=FALSE for html dependencies
This commit sets all_files=FALSE for html dependencies in www/shared.
2021-11-04 11:24:46 -05:00
Winston Chang
33ed698e5b Remove “file =“ 2021-11-04 11:15:01 -05:00
Winston Chang
ed547fdf40 Rebuild JS files 2021-11-03 21:28:15 -05:00
Winston Chang
0b1c35c92b Use .map() and .forEeach() instead of for loops 2021-11-03 21:28:11 -05:00
Winston Chang
d304bdf333 Rename HtmlDepSimplified to HtmlDepNormalized 2021-11-03 20:54:53 -05:00
Winston Chang
a9255e6b12 Extract jquery-ui dependency into function 2021-11-03 20:54:53 -05:00
Winston Chang
45429fb798 Streamline code for adding scripts and attachments 2021-11-03 13:56:37 -05:00
Winston Chang
1206d1d3ba Use array of objects to represent meta tags 2021-11-03 13:56:37 -05:00
Winston Chang
af44a447a1 Fix type specification of HTML dependency meta values 2021-11-03 11:40:49 -05:00
Winston Chang
d7fb6d1793 Fixes for strict null checks 2021-11-03 11:40:49 -05:00
Winston Chang
cb0083adb2 Update VSCode editor settings 2021-11-03 11:40:49 -05:00
Winston Chang
77bae68f26 Update NEWS 2021-11-03 11:40:48 -05:00
Winston Chang
e9f8b4d552 Use htmlDependencies with ‘src=file’ for showcase mode 2021-11-03 11:39:28 -05:00
Winston Chang
aee6b74cfb Minor cleanups 2021-11-03 11:39:28 -05:00
Winston Chang
29b6b03297 Extract restyle code into separate code path 2021-11-03 11:39:28 -05:00
Winston Chang
b5ebd8a645 Fix attachment handling and add more specific types 2021-11-03 11:39:28 -05:00
Winston Chang
356ba8c5a1 Add simplifyHtmlDependency() function 2021-11-03 11:39:28 -05:00
Winston Chang
5aa5cb1794 Make dep.src.href field optional 2021-11-03 11:39:28 -05:00
Winston Chang
09c609e417 Bump version to 1.7.1.9001 2021-11-03 11:39:28 -05:00
Winston Chang
10e7d11846 Allow array of attributes for stylesheets 2021-11-03 11:39:28 -05:00
Winston Chang
4e442312a7 Serve HTML dependencies from dynamic paths 2021-11-03 11:39:28 -05:00
Winston Chang
8ea97df3f2 Fix getid access of data-input-id attribute (#3538) 2021-11-03 11:38:54 -05:00
Winston Chang
a8c14dab96 Bump version to 1.7.1.9000 2021-10-04 11:09:26 -05:00
Winston Chang
00775b90e8 Bump version to 1.7.1 2021-09-30 14:48:12 -05:00
Winston Chang
c6ae4c0034 Update NEWS 2021-09-30 14:48:06 -05:00
Winston Chang
1efcaa0b5d Use esbuild option preserveSymlinks
This allows the node_modules directory to be a symlink, without causing weird
build problems.
2021-09-30 14:44:17 -05:00
Carson Sievert
e6d94f6f66 Fix regression in repeated appendTab()s when navbarMenu() is present (#3518) 2021-09-30 14:43:09 -05:00
Barret Schloerke
5a8a02626c add news item for #3512 2021-09-28 18:00:36 -04:00
Hadley Wickham
c23293750d Re-arrange conditions for testthat 1.0.0 compatibility (#3512) 2021-09-28 17:51:45 -04:00
Winston Chang
9de74048a2 Bump version to 1.7.0.9000 2021-09-28 16:39:35 -05:00
wch
0fc861afb4 yarn build (GitHub Actions) 2021-09-10 20:05:12 +00:00
wch
2300dae10b sync package version (GitHub Actions) 2021-09-10 20:02:58 +00:00
wch
dfbb98abfd Document (GitHub Actions) 2021-09-10 20:02:19 +00:00
Winston Chang
9670839235 Fix example parse errors 2021-09-10 14:55:53 -05:00
Winston Chang
1e2326c2b6 Update reexports 2021-09-10 14:46:30 -05:00
Carson
6f46b847e2 Address check NOTE: Undeclared package ‘htmlwidgets’ in Rd xrefs 2021-09-07 15:10:52 -05:00
Carson
8c44559a1f Fix DT Rd link 2021-09-07 14:48:34 -05:00
Carson
d245a972ee simplify gha 2021-09-07 14:23:22 -05:00
Carson
c153d0591f bump version 2021-09-07 14:14:50 -05:00
Barret Schloerke
2ce18ef324 Update GHA workflows to use latest versions (#3492) 2021-08-24 14:59:52 -04:00
Barret Schloerke
2792d65e40 Fix link to DT::renderDataTable() (#3490) 2021-08-20 09:14:57 -05:00
Barret Schloerke
7b00f605aa Remove rlang remote (#3487) 2021-08-19 18:07:23 -04:00
Barret Schloerke
4cb3f05e8e Adjust app port tests to use random port values (#3488) 2021-08-19 18:06:43 -04:00
Winston Chang
8e40c815eb Merge pull request #3485 from rstudio/wch-fix-checkbox-radio 2021-08-13 13:56:35 -05:00
Winston Chang
6dfd8bc0ff Only ignore node_modules at top level 2021-08-13 13:50:54 -05:00
wch
2ef397f024 yarn build (GitHub Actions) 2021-08-13 18:08:05 +00:00
wch
94749f6114 yarn lint (GitHub Actions) 2021-08-13 18:08:05 +00:00
Winston Chang
4a39588d00 Update NEWS 2021-08-13 13:00:57 -05:00
Winston Chang
f5d5832149 Fix invisible checkboxes and radio buttons in RStudio on Mac 2021-08-13 12:58:12 -05:00
Barret Schloerke
68deab9b0e Remove console.log("Shiny version: ", Shiny.version) statement (#3480) 2021-08-05 18:16:39 -04:00
Winston Chang
96efac2bd7 Merge pull request #3478 from rstudio/install_expr_news_entry 2021-08-05 10:17:50 -05:00
Barret Schloerke
a67059f9f9 update news 2021-08-04 15:31:56 -04:00
Barret Schloerke
cdc51c09c7 Add test for inject()ed quosures when extracting the cacheHint (#3476) 2021-08-04 13:35:50 -04:00
Barret Schloerke
a6f02cf214 Fix bash logic in action step (#3474) 2021-08-02 22:35:11 -04:00
Barret Schloerke
7600770a6e Fix Rituals workflow validating commits have not been made (#3473) 2021-08-02 22:19:31 -04:00
Barret Schloerke
1b3ed88bd1 exprToFunction() and installExprFunction() support quosures (#3472)
Co-authored-by: Barret Schloerke <schloerke@gmail.com>
Co-authored-by: Winston Chang <winston@stdout.org>
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
Co-authored-by: Joe Cheng <joe@rstudio.com>
2021-08-02 22:09:19 -04:00
Barret Schloerke
f01dc9f0fb Move user documentation up to the top of Readme (#3464) 2021-08-02 22:06:09 -04:00
Barret Schloerke
9a65890e92 Update esbuild-plugin-sass to latest version (#3463) 2021-08-02 21:41:02 -04:00
Carson Sievert
ffef0c2eb1 Interpret NULL discrete limits as NA, fixes #2666 (#2668)
Co-authored-by: Winston Chang <winston@stdout.org>
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-07-27 14:02:58 -05:00
Barret Schloerke
8b74338b0f Add sustainEnvAndQuoted(). Remove getQuosure() (#3468)
Documentation to come in a later PR

Co-authored-by: Barret Schloerke <schloerke@gmail.com>
Co-authored-by: Winston Chang <winston@stdout.org>
2021-07-26 17:54:37 -04:00
Winston Chang
ed3c676548 Merge pull request #3466 from rstudio/wch-exprfunction-fix 2021-07-16 16:14:38 -05:00
Winston Chang
30c0a2bd29 Update NEWS 2021-07-16 15:17:37 -05:00
Winston Chang
997e5e5ce5 Fix handling of getQuosure3(expr, env, quoted=TRUE) 2021-07-16 15:14:28 -05:00
Winston Chang
aba6b2e4db Fix NEWS entry 2021-07-15 17:52:59 -05:00
Winston Chang
3f48e3b0af Merge pull request #3462 from rstudio/wch-exprfunction-quosures 2021-07-15 17:51:22 -05:00
Winston Chang
b4879a342c Update NEWS 2021-07-15 17:50:40 -05:00
Winston Chang
5070146061 Fix example 2021-07-15 17:21:15 -05:00
Winston Chang
d28c3e15ad Update pkgdown.yml 2021-07-15 17:14:47 -05:00
Winston Chang
4b496be520 Update documentation 2021-07-15 17:11:36 -05:00
Winston Chang
979288a590 Add quosure tests for custom render functions 2021-07-14 16:31:23 -05:00
Winston Chang
9365d4f3c4 Update comment 2021-07-14 16:30:53 -05:00
Winston Chang
e1daf8aae7 Export getQuosure() and add internal getQuosure3() 2021-07-09 17:36:46 -05:00
Winston Chang
8a57dbf608 Rename get_quosure to getQuosure 2021-07-06 12:36:45 -05:00
Winston Chang
ac9b76c651 Modify exprToFunction to accept quosures 2021-07-02 15:45:10 -05:00
Winston Chang
139526ef2d Move expression/quosure functions to utils-lang.R 2021-07-02 14:25:46 -05:00
Winston Chang
d1e7e6c63a Add note about R version support 2021-07-02 14:04:35 -05:00
Winston Chang
29b574bf94 Merge pull request #3456 from heds1/update-rejected-ports 2021-07-01 16:34:25 -05:00
Barret Schloerke
7e4248bbca TypeScript: Globally declare Shiny variable, window.Shiny variable, and Shiny type (#3457) 2021-07-01 14:51:16 -04:00
heds1
fee267dc2e docs: update runapp port parameter docs, and add three more tcp ports to be blocked 2021-07-01 21:40:59 +12:00
Carson Sievert
9864130435 Use random inline styles to ensure transitionend fires everytime (#3452)
* Follow up to #3333: use random inline styles to ensure transitionend fires everytime

* yarn lint (GitHub Actions)

* Add missing '#'

* yarn lint (GitHub Actions)

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-06-30 15:26:49 -05:00
Carson Sievert
c9770cbd03 Close #3443: Fix sliderInput()'s grid tick positioning without Bootstrap (#3444) 2021-06-29 15:56:47 -05:00
Carson Sievert
ed6a40ba41 Close #3446: get removeModel() working with Bootstrap 4 (#3447) 2021-06-29 15:54:48 -05:00
Carson
3c22cdf90c roxygenize 2021-06-29 15:11:39 -05:00
Marcus Spittler
e55749b897 Update utils.R example to validate() (#2809)
Added an empty option to `choices` in `selectizeInput` in order to make the second `need` statement in `validate` meaningful. Otherwise the second `need` ("Please choose a state") is never displayed.
2021-06-29 15:10:42 -05:00
Carson Sievert
88cd87a5f7 Revert "Set selectize dropdownParent to "body" to prevent clipping" (#3450)
This reverts commit ce90d5cd0a.
2021-06-29 12:22:33 -05:00
Barret Schloerke
244fdc72bc Leverage more eslint rules (#3439) 2021-06-22 21:20:54 -04:00
Barret Schloerke
b9d163a71d TypeScript other distributed JS/CSS files (#3436) 2021-06-18 10:18:51 -04:00
Barret Schloerke
61ee467dee Replace dev versions with -alpha versions for JS code (#3435) 2021-06-17 16:02:39 -04:00
Carson Sievert
7c0829d553 Change from .nav-item to .dropdown-item when inserting inside .dropdown-menu (#3434)
* Change from .nav-item to .dropdown-item when inserting inside .dropdown-menu

* Update srcts/src/shiny/shinyapp.ts

* Update srcts/src/shiny/shinyapp.ts

* yarn lint (GitHub Actions)

* yarn build (GitHub Actions)

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-06-16 17:44:03 -05:00
Carson Sievert
68eb4c6965 update news for breaking insertTab() change (#3433) 2021-06-16 16:41:07 -05:00
Barret Schloerke
6d4015f61b ./package.json updates to make TS Types package cleaner to install (#3430)
Co-authored-by: Barret Schloerke <schloerke@gmail.com>
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2021-06-16 16:08:50 -04:00
Barret Schloerke
d89513b7e0 Match casing for plot alt text "Plot object" (#3432)
* Match spelling for Plot Object phrase

From #3398

* Document (GitHub Actions)

* Consistent casing for `"Plot object"` for plot alt text

Co-authored-by: schloerke <schloerke@users.noreply.github.com>
2021-06-16 15:08:13 -05:00
Carson Sievert
a159594a45 insertTab(position = "after") by default (#3431)
* Follow up to #3404: change insertTab()'s default position so that default behavior doesn't change

* Update news

* Document (GitHub Actions)

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-06-16 15:01:36 -05:00
Carson Sievert
78c62ad819 Various cleanup (#3428)
* Follow up to #3366: don't change sliderInput()'s default accent color

* Update news

* nav_append not tab_append 🤦

* bslib no longer tries to mark a non-tabPanel as active
2021-06-15 16:45:23 -05:00
Barret Schloerke
b3247d5a3b Move ./srcts configs to top level to support types installation from GitHub (#3425) 2021-06-15 14:18:53 -04:00
Winston Chang
91f920e14c Merge pull request #3413 from rstudio/feature/selectize-dropdown-parent-body
Set selectize dropdownParent to "body" to prevent clipping
2021-06-15 11:50:37 -05:00
Carson Sievert
bcb7cde44b insertTab() now handles position correctly when target is NULL (#3404)
* Close #3403: insertTab() now handles position correctly when target is NULL

* Have insertTab()'s target default to NULL

* yarn tsc (GitHub Actions)

* yarn build (GitHub Actions)

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-06-14 15:51:38 -05:00
Carson Sievert
052c9458b7 yarn add node-gyp; yarn build (#3424) 2021-06-14 15:51:03 -05:00
Barret Schloerke
3fe8c27d21 Export TypeScript type definitions to local folder (#3418) 2021-06-14 14:25:05 -04:00
Barret Schloerke
1dd256b210 TypeScript: Remove any types / improve type definitions (#3414) 2021-06-14 14:22:39 -04:00
Carson Sievert
dc9c6ae769 Better color constrasting in sliderInput() (#3366)
* Better color constrasting in sliderInput()

Closes https://github.com/rstudio/bslib/issues/228

* Update build script; recompile

* bslib tabsets now include data-bs-toggle
2021-06-14 12:48:57 -05:00
Carson Sievert
2cdafed2e0 Use ggplot2::get_alt_text() if available to provide better default alt text (#3398)
* Close #3397: Use ggplot2::get_alt_text() if available to provide more informative default alt text for ggplots in renderPlot()

* Update R/render-plot.R

Co-authored-by: Winston Chang <winston@stdout.org>

* better Rd docs

* make logic more self-contained

* Add news

Co-authored-by: Winston Chang <winston@stdout.org>
2021-06-14 10:22:07 -05:00
JJ Allaire
ce90d5cd0a Set selectize dropdownParent to "body" to prevent clipping
To prevent clipping of the selectize drop-down we set the dropdownParent to "body". This might be necessary if e.g. overflow-x: scroll is set on it's container, which forces overflow-y to 'auto' (as per https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-y).

See option docs here: https://github.com/selectize/selectize.js/blob/master/docs/usage.md

Additional discussion of usage here: https://github.com/selectize/selectize.js/issues/192
2021-06-09 19:41:10 -04:00
Barret Schloerke
b4caa9137d Distribute TypeScript code into separate files (#3317)
Co-authored-by: Barret Schloerke <schloerke@gmail.com>
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2021-06-09 14:54:47 -04:00
Carson Sievert
dcca77c936 Fix tab input value updating for BS4 dropdowns (#3412)
* Fix tab input value updating for BS4 dropdowns

* Add comments

* yarn build (GitHub Actions)

* Better comment

* yarn lint (GitHub Actions)

* yarn build (GitHub Actions)

Co-authored-by: Barret Schloerke <barret@rstudio.com>
Co-authored-by: schloerke <schloerke@users.noreply.github.com>
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-06-02 15:55:32 -05:00
Carson Sievert
871b1baacc Follow up to #3410: bump version and update news (#3411) 2021-06-02 13:03:09 -05:00
Carson Sievert
4deb699066 Bootstrap 5 support (#3410)
* Bootstrap 5 support for modals & showcase mode

* selectizeInput() BS5 compatibility

* Both BS4 and 5 define window.bootstrap

* Document (GitHub Actions)

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-06-02 12:36:04 -05:00
Carson Sievert
ccc8e053c6 Use bslib's new nav() api to implement tabPanel() and friends (#3388)
* Use bslib's new nav() api to implement tabPanel() and friends

* bslib won't be re-exporting prepend/append tab since they've been superceded by insertTab()

* Update DESCRIPTION

* Use the new bslib::page_navbar()

* Leverage bslib::page_navbar()'s more intelligent title->windowTitle handling

Closes #2310

* fix name change

* Make sure navbarPage() isn't browsable by default
2021-06-02 12:10:41 -05:00
Barret Schloerke
6405056c92 Install Cairo macOS devel brew dependency (#3408) 2021-05-26 17:06:27 -04:00
Barret Schloerke
9f9304fdc5 Remove Font Awesome 5 message about the level-up icon (#3407) 2021-05-26 16:31:15 -04:00
Carson Sievert
3d3b05c7a5 Correctly render script tags defined as list() objects (#3395)
* Close #3345: correctly render script tags defined as list() objects

* implement boolean attrs; use vanilla JS

* Update news

* avoid toggleAttribute

* yarn lint (GitHub Actions)

* code review

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-05-20 17:44:34 -05:00
Carson Sievert
543a6b5836 Fix CRAN note (#3394)
* Follow up to #3392: update tabPanel() baselines

* Fix check NOTE

* Revert "Follow up to #3392: update tabPanel() baselines"

This reverts commit c97022c386.

Issue will be fixed in htmltools
2021-05-20 13:58:16 -05:00
Carson Sievert
b0de68919a tagify() dynamic UI before attempting to write the tags (#3392)
* Close #3391: properly tagify() dynamic UI before attempting to write the tags

* Document (GitHub Actions)

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-05-18 12:28:17 -05:00
Richard Iannone
d65ad5ea90 Modify icon() function to call fontawesome::fa_i() for equivalent functionality (#3302)
* Use `fontawesome::fa_i()` for FA <i> tags

* Remove fontawesome vendor files

* Add fontawesome pkg to Imports & Remotes

* Remove FontAwesome `person()` entry

* Remove Font Awesome license info

* Delete font-awesome.R

* Update 'Collate' field (removes 'font-awesome.R')

* Delete updateFontAwesome.R

* Prefer use of `fontawesome::fa()`

* Improve function documentation

* Update help file using roxygen

* Modify icon name

* Update icon name in example

* Modify icon name in example

* Update help files

* Update bootstrap.R

* Update icon.Rd

* Update bootstrap.R

* Revert `showcaseCodeTabs()` to use FA v4 name

* Revert icon name in example (back to FA v4)

* Remove `call. = FALSE` in `stop()`

* Remove `fontawesome` from Remotes

* Add min version req for the fontawesome pkg

* Increase minimum version requirement for fontawesome

* Update roxygen docs for `icon()`

* Document (GitHub Actions)

* Update icon.Rd

* Generate early return <i> tag for tabsetPanel logic

* Close #3384 and #3383: simplify and correct icon() logic

* Install htmltools PR for now

* Document (GitHub Actions)

* Avoid using tag attribs to hold non-attribute values

* Better legacy support

* No need to call prepTabIcon() twice

* code review

* Fix glyphicon class creation

* update news

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
Co-authored-by: rich-iannone <rich-iannone@users.noreply.github.com>
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-05-12 14:26:09 -05:00
Carson Sievert
383fa6c0e0 Follow up to #3372: fix oversight in refactor (#3387) 2021-05-10 09:58:30 -05:00
Barret Schloerke
8d40b3af70 Revert "Do not double pull within rituals"
This reverts commit 9c80d7a4ec.
2021-05-10 10:49:54 -04:00
Barret Schloerke
9c80d7a4ec Do not double pull within rituals 2021-05-07 15:15:41 -04:00
Winston Chang
2360bde13e Remove deprecated code and parameters (#3137)
* Remove deprecated reactive* functions

* Remove deprecated code

* Update NEWS

* Remove extractStackTrace and formatStackTrace

* remove responsive from bootstrapPage() wrappers

* Move extractStackTrace() to tests so they pass

* Don't force suggested pkgs in devel on GHA

Co-authored-by: Carson <cpsievert1@gmail.com>
2021-05-06 09:46:30 -05:00
Winston Chang
d25ae099d4 Merge pull request #3333 from rstudio/reportCssOnLoad 2021-05-05 18:12:17 -05:00
Winston Chang
2d492886e4 Prettify TS 2021-05-05 18:04:35 -05:00
Winston Chang
33741436c7 Merge remote-tracking branch 'origin/master' into reportCssOnLoad 2021-05-05 18:03:41 -05:00
Winston Chang
318cc7fcaf Rebuild JS files 2021-05-05 18:02:32 -05:00
Winston Chang
bebcf0b196 Add important flag 2021-05-05 18:02:32 -05:00
Winston Chang
f2be2e4eb1 Update comments 2021-05-05 17:48:39 -05:00
Winston Chang
a2ea017046 Add sendImageSize2 2021-05-05 17:40:46 -05:00
Winston Chang
fc338c8958 Use removeSheet() 2021-05-05 16:17:34 -05:00
Winston Chang
bbb27f1224 Make more CSS properties !important 2021-05-05 15:56:50 -05:00
Winston Chang
d2fbdb6c48 Add note about synchronous behavior in IE 2021-05-05 15:56:29 -05:00
Winston Chang
38c70842d9 Rebuild JS files 2021-05-05 15:18:01 -05:00
Winston Chang
0e22c4c591 Simplify IE CSS handling 2021-05-05 14:53:12 -05:00
Winston Chang
70e0eede16 New strategy for sending information when CSS loads 2021-05-05 14:50:39 -05:00
Barret Schloerke
4858a379e7 Make sure dev version of rlang is available (#3382) 2021-05-05 15:01:47 -04:00
Carson Sievert
3e33755a9e Reduce complexity and 'black-boxed' nature of tab panel logic (#3372)
* Follow up to #3315: reduce complexity and 'black-boxed' nature of tab panel logic

* asTags(selected = FALSE) is now root()

* tagAddRenderHook

* Add bslib to remotes

* Document (GitHub Actions)

* root() was recently changed to allTags()

* code review

* tagQuery() doesn't necessarily preserve order of attributes

* place href attribute before data attributes

* add nav-item/nav-link to BS4+ dropdowns

* Make sure .nav-item is removed in .dropdown-menu

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-05-05 10:05:24 -05:00
Carson Sievert
f2ad004f33 Install dev version of rlang (#3379)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
Co-authored-by: Barret Schloerke <schloerke@gmail.com>
2021-05-05 10:44:19 -04:00
Carson
16e0d9e355 Comment about the hoisting 2021-05-04 14:33:54 -05:00
Carson
d430b80191 Use sendImageSize instead of Shiny.bindAll to resend CSS info 2021-05-04 14:19:40 -05:00
Carson
2ffa8707ea Merge branch 'master' into reportCssOnLoad 2021-05-04 12:13:37 -05:00
Carson
cbd06cbd8e Merge branch 'master' into reportCssOnLoad 2021-05-04 12:11:56 -05:00
Carson Sievert
d3aa1acfbf Use tab instead of tooltip constructor to check Bootstrap version (#3377)
Closes https://github.com/rstudio/shinycoreci-apps/issues/138
2021-05-04 08:15:27 -05:00
Winston Chang
c2232ae07a Merge pull request #3373 from rstudio/nested-quo-to-func 2021-04-27 12:38:33 -05:00
Winston Chang
cf0a865d6f Remove ... args from function 2021-04-27 12:29:34 -05:00
Carson
4942b3e6ad Add news item 2021-04-22 16:49:40 -05:00
Joe Cheng
f374a1512a Fix rlang::inject with render functions
Render functions use quoToFunction() to convert quosures to
functions; quoToFunction() was using new_function, which leads
to non-tidy evaluation, so nested quosures are not evaluated.

See https://github.com/rstudio/shiny/pull/3361#issuecomment-820672180
2021-04-22 11:57:24 -07:00
Barret Schloerke
1558c848f4 Export register_devmode_option() (#3364) 2021-04-20 17:33:58 -04:00
Barret Schloerke
4a2bb8fc43 Add ORCID info (#3363) 2021-04-09 16:35:49 -04:00
Barret Schloerke
fad21af146 Make external libs builder leveraging esbuild (#3357) 2021-04-07 16:06:05 -04:00
Winston Chang
850a628978 Fix variable name 2021-04-06 12:52:14 -05:00
Winston Chang
4d2311841d Merge pull request #3334 from rstudio/boostrapPageJQuery
bootstrapPage() now includes jQuery so static rendering works as expected
2021-04-06 12:10:12 -05:00
Winston Chang
5c4175cd5f Merge pull request #3353 from rstudio/wch-fix-tab-title 2021-04-02 14:51:44 -05:00
Winston Chang
2931e40c7b Update 2021-04-02 14:50:36 -05:00
Winston Chang
6a6eae1ce1 Update R/bootstrap.R
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2021-04-02 14:49:37 -05:00
Winston Chang
210642e96c Update R/bootstrap.R
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2021-04-02 14:49:26 -05:00
Winston Chang
c97fad30ef Fix html tags in tab titles 2021-04-02 13:27:22 -05:00
Carson Sievert
268c9afec3 Close #3299: bootstrapLib() should always call setCurrentTheme() when shiny is running (#3300) 2021-03-26 15:12:54 -05:00
Carson Sievert
5c919ae565 Make ensureTabsetHasVisibleTab() is aware of BS4+ markup (#3349)
* Close https://github.com/rstudio/shinycoreci-apps/issues/126: Make ensureTabsetHasVisibleTab() aware of BS4+ markup

* yarn build (GitHub Actions)

* Update srcts/src/main.ts

* yarn lint (GitHub Actions)

* yarn build (GitHub Actions)

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-03-26 15:11:48 -05:00
Hadley Wickham
e29d92c5ff Allow trailing commas in more places (#3328)
* Allow trailing commas in more places

I grepped for list(...) and replaced with rlang::list2(...). This also enables !!! which is generally not important for Shiny because it automatically splices lists/tagLists, but I doubt it will affect any existing code.

* update news; no need to rlang::

* missed one

* Update NEWS.md

Co-authored-by: Hadley Wickham <h.wickham@gmail.com>

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2021-03-23 14:24:21 -05:00
Carson
0a331e3366 update news 2021-03-22 16:00:39 -05:00
Carson Sievert
32d0e146ad Various improvements to tab panels (#3315)
* 'Native' Bootstrap 4 tabset panel support

* downgrade error to warning; improve the messaging

* Make tab anchor selectors more a bit more sensible and consistent across versions

* More of the same

* fix silly bug

* Be more careful about unpacking a .nav-item into a .dropdown-item

* Keep refactoring R logic to make it cleaner and easier to reuse elsewhere

* Go back to the purely class based CSS selectors for BS4 tab input

* Keep supporting off-label behavior of shiny.tag getting transformed into 'empty' nav/tab

* Add header and footer args to tabsetPanel()/navlistPanel() since there is precedence in navbarPage() and mention them in the warning

* Drop NULLs instead of creating an empty nav from them, closes #1928

* Remove tabPanelMenu() alias

* Add a card argument for wrapping content in a card

* Throw an error if card=T is used outside of a BS4+ context

* No more tabPanelMenu() alias

* Document (GitHub Actions)

* Port JS changes to TypeScript

* Allow liTag to be assigned a new value

* abort() is no longer being used

* Add some unit tests

* Document the new card argument

* Get tests passing on older R versions

* Get tests passing on older R versions

* Get tests passing on older R versions

* Skip snapshots on R < 3.6

* require dev version of htmltools

* remove card argument (at least for now)

* Document (GitHub Actions)

* Update tests/testthat/test-tabPanel.R

Co-authored-by: Winston Chang <winston@stdout.org>

* Have processDeps() call renderTags() on tagFunction() objects

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
Co-authored-by: Winston Chang <winston@stdout.org>
2021-03-22 12:37:57 -05:00
Carson
c94f411fc6 Specify both href and file 2021-03-12 17:17:01 -06:00
Carson
22d408aa7b Close #3316: bootstrapPage() now includes jQuery so static rendering works as expected 2021-03-12 16:50:24 -06:00
Carson
a44fdc1b11 remove logs 2021-03-11 17:01:25 -06:00
Carson
50ca830ec6 Poll every 0.1sec for 10secs 2021-03-11 16:57:20 -06:00
Carson
e643cd3824 request on next tick 2021-03-11 12:37:41 -06:00
Carson
2660a50d31 try using requestAnimationFrame 2021-03-11 11:46:17 -06:00
Carson
927912efe3 Try sending multiple times 2021-03-11 11:13:51 -06:00
Carson
9b49a24e74 Try not debouncing 2021-03-11 11:03:28 -06:00
Carson
0824b22532 Add debug statements 2021-03-11 10:49:16 -06:00
Carson
000feead00 When refreshing a stylesheet, schedule a report CSS values once the sheet is loaded 2021-03-11 09:50:49 -06:00
Winston Chang
d582e53f73 Merge pull request #3311 from rstudio/update-jquery-3.6.0
Co-authored-by: Barret Schloerke <barret@rstudio.com>
Co-authored-by: Barret Schloerke <schloerke@gmail.com>
2021-03-03 09:47:47 -06:00
Barret Schloerke
52ad7d12cb jquery@3.6.0 is available. @types/jquery@3.6.0 is not ready yet. 2021-03-03 10:42:15 -05:00
Barret Schloerke
10810308f0 Install jquery and @types/jquery in ./srcts 2021-03-03 10:37:57 -05:00
Carson Sievert
4ce1058448 Remove old @param theme roxygen documentation and rely in @inheritParams bootstrapPage (#3312) 2021-03-02 16:34:35 -06:00
Winston Chang
0db06df77f Automatically record jQuery version 2021-03-02 13:56:14 -06:00
Winston Chang
fdca53d4d2 Update to jQuery 3.6.0 2021-03-02 13:40:33 -06:00
Barret Schloerke
8395598328 🤦 2021-02-25 15:56:43 -05:00
Barret Schloerke
1b8635db32 Initialize TypeScript (#3296) 2021-02-25 15:44:11 -05:00
Winston Chang
60db1e02b0 Merge pull request #3269 from rstudio/read-output
Tweak errors when reading from outputs
2021-02-12 15:31:45 -06:00
Winston Chang
a86e9c3609 Merge pull request #3287 from rstudio/validate-req-truthy
Tweaks to validate(), req(), and isTruthy() docs
2021-02-12 15:09:01 -06:00
Winston Chang
6d77b22f97 Add isTruthy to pkgdown 2021-02-12 14:59:15 -06:00
Winston Chang
e1b3756166 Merge pull request #3272 from rstudio/slider-dep
Remove deprecated arguments to sliderInput
2021-02-12 14:35:51 -06:00
Winston Chang
edf354f416 Merge pull request #3288 from rstudio/update-fa 2021-02-10 12:06:43 -06:00
Winston Chang
954a979a83 Add note about auto-generated code 2021-02-10 12:00:59 -06:00
Winston Chang
fe9a87fb06 Update NEWS 2021-02-10 09:58:44 -06:00
Winston Chang
1842a15f74 Update to Font-Awesome 5.15.2 2021-02-10 09:57:16 -06:00
Winston Chang
a568238472 Update font-awesome update script 2021-02-10 09:56:45 -06:00
Hadley Wickham
fa200022c5 Tweaks to validate(), req(), and isTruthy() docs
* Use more markdown/roxygen2 tags
* Pull isTruthy out into own file
* Rewrite validate for clarity
2021-02-09 17:37:36 -06:00
Winston Chang
a6347341e3 Merge pull request #3176 from rstudio/wch-faststack 2021-02-09 14:56:13 -06:00
Winston Chang
c41481e488 Merge pull request #3285 from rstudio/dotloop-bug
Fix logic bug in dotloop()
2021-02-09 14:51:19 -06:00
Winston Chang
767abc3c0c Create restoreCtxStack in .onLoad() 2021-02-09 14:46:28 -06:00
Hadley Wickham
e005c24fbf Fix logic bug in dotloop()
Ensures that req() works without error
2021-02-09 13:56:09 -06:00
Winston Chang
8580f544fc Update NEWS 2021-02-09 11:57:25 -06:00
Winston Chang
2daa8ec944 Replace queues with fastqueue 2021-02-09 11:57:25 -06:00
Winston Chang
2b92014ea5 Use fastmap::faststack() and remove Stack 2021-02-09 11:57:19 -06:00
Winston Chang
f540679513 Merge pull request #2954 from rstudio/remove-test-context
Remove context() calls from example app
2021-02-09 11:54:22 -06:00
Shinya Uryu
d165cc6e8e Typo (#3283) 2021-02-05 08:43:13 -06:00
Winston Chang
c1878fe54f Merge pull request #3278 from rstudio/wch-fix-test 2021-02-02 11:01:07 -06:00
Winston Chang
f05948629e Adjust test time 2021-02-01 21:10:52 -06:00
Winston Chang
3e37dab4a1 De-functionize tests 2021-02-01 19:02:18 -06:00
Winston Chang
6584e1f960 Recommend using bindEvent() (#3277) 2021-02-01 18:11:30 -06:00
Hadley Wickham
64c5a67a0e Use testthat 3e (#3274) 2021-01-29 10:34:14 -06:00
Barret Schloerke
aea4e560ea Display devmode in docs (#3275) 2021-01-29 11:32:34 -05:00
Winston Chang
12554a0004 Add info about render functions with bindCache 2021-01-29 09:53:58 -06:00
Barret Schloerke
83336ef9a5 Update bootstrap-accessibility plugin (#3259)
* Copy from installed bslib location, no relative file path

* Adopt the fix from https://github.com/rstudio/bslib/pull/241

Co-authored-by: Carson <cpsievert1@gmail.com>
2021-01-27 09:39:10 -06:00
Hadley Wickham
08ab21b50e Remove deprecated arguments to sliderInput 2021-01-27 08:42:28 -06:00
Hadley Wickham
5628346ae1 Tweak errors when reading from outputs 2021-01-26 13:00:18 -06:00
Winston Chang
b165127d20 Merge pull request #3268 from rstudio/dt-docs 2021-01-26 12:49:23 -06:00
Hadley Wickham
905e2238d4 Drop tableOutput from ref index 2021-01-26 12:47:30 -06:00
Hadley Wickham
47bb1f657c Doc fixes 2021-01-26 12:47:30 -06:00
Hadley Wickham
c917d18d67 Improve table output docs
* Combine render + output functions in one file
* Put more info in the description
* Mild polishing of param docs
2021-01-26 12:47:30 -06:00
Winston Chang
93568cd53f Merge pull request #3264 from rstudio/wch-rm-digest 2021-01-26 10:22:35 -06:00
Winston Chang
6af06559f4 Update NEWS 2021-01-26 09:36:30 -06:00
Winston Chang
43239a0485 Use rlang::hash instead of digest 2021-01-26 09:36:30 -06:00
Winston Chang
e05f4097d6 Merge pull request #3267 from rstudio/slider-docs 2021-01-26 09:35:37 -06:00
hadley
35e62eaee9 yarn build (GitHub Actions) 2021-01-26 13:52:12 +00:00
Hadley Wickham
858c2e66e6 Clarify supported types in sliderRange() 2021-01-26 07:35:36 -06:00
Winston Chang
0d156171d4 Bump version to 1.6.0.9000 2021-01-25 15:56:38 -06:00
Winston Chang
b57cb6c8e1 Fix URLs 2021-01-19 11:15:00 -06:00
Carson
5ddff1bd37 Merge branch 'master' into rc-v1.6.0 2021-01-15 14:46:44 -06:00
Carson Sievert
036f923e05 Run accessiblity plugin JS when DOM is loaded (via defer attribute) a… (#3256)
* Use bslib's patched version of bootstrap-accessibility plugin (see https://github.com/rstudio/bslib/pull/224)

* Use new minified file

Co-authored-by: Barret Schloerke <schloerke@gmail.com>
2021-01-15 14:43:00 -06:00
Carson Sievert
130f4764a7 Documentation improvements for v1.6 (#3255) 2021-01-14 10:35:00 -06:00
Carson
c4b5e5f8a2 Merge branch 'master' into rc-v1.6.0 2021-01-13 14:23:56 -06:00
Barret Schloerke
ecb21df941 Use url checker (#3249)
* Update rituals.yaml

* update docs links

* Fix 404 link

* http://fontawesome.io to https://fontawesome.com

* Update links (GitHub Actions)

* Update NEWS.md

* Only check urls in rc branches

* missing paren

Co-authored-by: schloerke <schloerke@users.noreply.github.com>
2021-01-13 14:18:12 -06:00
Barret Schloerke
71d11ec103 Merge branch 'master' into rc-v1.6.0
* master:
  Reduce promises version to 1.1.0 and safeguard visibility test (#3252)
2021-01-12 13:31:09 -05:00
Barret Schloerke
213f0d3a93 Reduce promises version to 1.1.0 and safeguard visibility test (#3252) 2021-01-12 12:29:18 -06:00
Carson
8948eca0f3 Use checkJsCurrent.sh to rebuild JS 2021-01-08 14:37:39 -06:00
Carson
aa0c841aff Close #3244: sliderInput()'s handles are now always round 2021-01-08 14:22:14 -06:00
Carson
a8449382f0 Start shiny v1.6 release candidate 2021-01-05 13:52:16 -06:00
Carson Sievert
5b27d9258e Don't change the return value of bootstrapPage() if bslib isn't relevant (#3236)
* Close #3235: Don't change the return value of bootstrapPage() if bslib isn't relevant

Also, improved error message if theme is a character vector with 2 or more elements

* yarn build (GitHub Actions)

* bump version

* yarn build (GitHub Actions)

* Don't add an additional level to the returned tree structure

* More straightforward use of do.call()

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
Co-authored-by: Winston Chang <winston@stdout.org>
2021-01-05 13:21:38 -06:00
Winston Chang
2590cf3895 Drop GHA pr-commands and add GHA Rituals. Use pak to install (#3230)
Co-authored-by: Winston Chang <winston@stdout.org>
Co-authored-by: Barret Schloerke <schloerke@gmail.com>
2020-12-28 13:28:38 -05:00
Nick Strayer
a9f7068b2f bindCache() docs typos (#3232)
* vert -> very

* cachem::cache_mem() uses max_size argument to set size.

* Rebuilt docs after cache typo fixes

* Rerender docs with new roxygen version

* Installed cairo and rebuilt docs
2020-12-23 17:14:57 -06:00
Carson Sievert
1f9e4929a6 Follow up to #3228. shinyAppDir() now throws a classed condition when appDir is not a directory (#3229) 2020-12-23 10:20:48 -06:00
Carson Sievert
d56afca33e shinyAppDir() now throws an exception with a special class if no app.R/server.R file is found. (#3228)
shinytest:::is_app() can make use of this for better error reporting
2020-12-22 10:41:55 -06:00
Carson Sievert
8fa023b4ec Closes #223: Add selectize patch file to capture changes from #3217 (#3227) 2020-12-21 11:06:35 -06:00
Winston Chang
d9f73c4c6d Merge pull request #3212 from rstudio/wch-fix-selectize-enter 2020-12-21 10:42:55 -06:00
Winston Chang
68cf1c5410 Check for empty list 2020-12-21 10:40:24 -06:00
Winston Chang
a70220c6c4 Rebuild JS file 2020-12-19 23:02:20 -06:00
Winston Chang
99207d1d8f Simplify handling of empty options 2020-12-19 23:02:20 -06:00
Winston Chang
0baf2ecd70 Apply patches 2020-12-19 23:02:20 -06:00
Winston Chang
2c6f830223 Add patch for not triggering click on selectize 2020-12-19 23:02:20 -06:00
Winston Chang
98eb1b596d Update selectize-plugin-a11y comments 2020-12-19 23:02:20 -06:00
Winston Chang
145d222653 Add webdriver to remotes 2020-12-19 22:57:46 -06:00
Winston Chang
67e54572a8 Rebuild JS files 2020-12-19 22:52:01 -06:00
Winston Chang
3cc9b33a8d yarn upgrade 2020-12-19 22:51:24 -06:00
Barret Schloerke
12bc94fbc0 bump dev version to 1.5.0.9006 (#3221) 2020-12-19 16:32:22 -06:00
Winston Chang
b2379bfa5b Cache packages on Windows 2020-12-19 13:41:06 -06:00
Barret Schloerke
f4fc13fc2f Add devmode() features (#3174)
Co-authored-by: Barret Schloerke <schloerke@gmail.com>
Co-authored-by: Winston Chang <winston@stdout.org>
2020-12-18 14:31:31 -05:00
Nick Strayer
95081c43a7 Make sure setCurrentTheme() doesn't try and change bootstrap versions (#3210)
* Added check to make sure setCurrentTheme() doesn't try and change bootstrap version

* Update R/shiny.R

Style improvements via Carson

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

* Update error message to be more specific

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

* Make it clearer where the new bs_theme() call should be made.

* Add a check to make sure setCurrentTheme() receive a bs_theme() object

Co-authored-by: Nick Strayer <nick.strayer@gmail.com>
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2020-12-18 13:28:16 -06:00
Winston Chang
bb3b3d5a47 Use rlang from CRAN 2020-12-18 12:39:29 -06:00
Carson Sievert
f635f98ccb Put pre-Chromium Edge in the same category as IE (#3220)
* Put pre-Chromium Edge in the same category as IE, closes #3219

* code review
2020-12-18 12:08:50 -06:00
Carson Sievert
eef44295db Close #3215, selectize's active item fg color now uses bslib's color-contrast() for more generalized contrasting (#3217) 2020-12-18 12:01:44 -06:00
Barret Schloerke
5e1afc61c1 Update private$currentThemeDependency label to be Theme Counter (#3206)
* Update `private$currentThemeDependency` label to be `Theme Iteration`

* Update R/shiny.R

Co-authored-by: Winston Chang <winston@stdout.org>
2020-12-16 15:31:50 -06:00
Carson Sievert
8edcbb3dc1 Revert "Make .shiny-text-output more aware of Bootstrap's pre styles (#3203)" (#3209)
This reverts commit 4eeb4a12a7.

This change was superfluous given that bslib's bs3compat CSS will provide <pre> styles and reduces the risk of overriding user rules targetting .shiny-text-output (https://github.com/rstudio/shinycoreci-apps/issues/95)
2020-12-16 13:41:01 -06:00
Malcolm Barrett
dca3722cb8 fix typo in docs (#3204)
Co-authored-by: runner <runner@Mac-1607960235106.local>
2020-12-16 13:37:16 -06:00
Stéphane Guillou
7eb0e93731 typos in documentation (#3205)
Co-authored-by: runner <runner@Mac-1607961254343.local>
2020-12-16 13:36:32 -06:00
Carson Sievert
6034c3ff7a Resend CSS styles when relevant element(s) mutate. (#3198) 2020-12-16 11:59:37 -06:00
Carson Sievert
4eeb4a12a7 Make .shiny-text-output more aware of Bootstrap's pre styles (#3203) 2020-12-11 18:55:27 -06:00
Winston Chang
6daa689888 Merge pull request #3201 from rstudio/wch-fix-datepicker 2020-12-11 15:51:46 -06:00
Winston Chang
cded44b40a Update NEWS 2020-12-11 15:51:08 -06:00
Winston Chang
290c9f6b20 Rebuild JS files 2020-12-11 15:51:08 -06:00
Winston Chang
be3d712fdf Set min/max date before setting value. Closes #3197 2020-12-11 15:51:08 -06:00
Winston Chang
f5666bcba1 Respect shiny.minified for bootstrap-datepicker.js 2020-12-11 10:02:01 -06:00
Winston Chang
f3c89bed01 Merge pull request #3199 from rstudio/fix-session-validate
Co-authored-by: Carson <cpsievert1@gmail.com>
2020-12-10 18:06:12 -06:00
Carson
9b0f170730 Skip POSIXlt slider tests on R3.6 and below 2020-12-10 15:24:07 -06:00
Winston Chang
74350cd443 Update NEWS 2020-12-10 12:28:46 -06:00
Winston Chang
61aa7bb3b0 validate_session_object: Also work with modules 2020-12-10 12:25:30 -06:00
Winston Chang
82fdbeda49 Fix test 2020-12-10 12:25:12 -06:00
Winston Chang
196b220faf All session parameters from the update* functions now default to getDefaultReactiveDomain() (#3195)
Co-authored-by: colin <colin@thinkr.fr>
2020-12-08 12:18:37 -06:00
Hadley Wickham
f41c484913 Respect reactiveConsole() in new errors (#3193) 2020-12-08 12:17:43 -06:00
Carson Sievert
a1a20b3f4b Add NEWS notes for #3042 and #3038 (#3191)
Co-authored-by: Winston Chang <winston@stdout.org>
2020-12-08 10:59:23 -06:00
Winston Chang
bbf9bee28e Add a warning message when value < min | value > max in sliderInput (#3194)
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
Co-authored-by: colin <colin@thinkr.fr>
Co-authored-by: Colin Fay <contact@colinfay.me>
2020-12-08 10:55:18 -06:00
Winston Chang
24a1ef9594 Clear selected date if not within min/max range (#3188) 2020-12-07 09:13:48 -06:00
Carson Sievert
c5adef0a05 Add 'auto' brush fill and stroke (#2864)
* Add 'auto' brush fill and stroke

* getStyle() from utils

* Update getThematicOption()

* Use getThematicOption() helper in startPNG(), too
2020-12-04 16:49:08 -06:00
Carson Sievert
508c197446 getCurrentOutputInfo() bugfix (#3189) 2020-12-04 16:38:48 -06:00
Carson Sievert
473ec834fe radioButtons() and checkboxGroup() accessibility (#3187)
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
Co-authored-by: JooYoung Seo <sjysky@gmail.com>
2020-12-04 15:53:53 -06:00
Carson Sievert
66968904bf Cleaner logic for conditional CSS styles (#2671)
* Cleaner logic for conditional CSS styles

It's really only plotOutput() that behaves differently;
previously it was not possible to specify a NULL width
or height and not get broken styles

* require dev version of htmltools

Co-authored-by: Joe Cheng <joe@rstudio.com>
2020-12-04 15:52:50 -06:00
Hadley Wickham
f169792e59 Experiment with error message (#3007) 2020-12-04 14:20:30 -06:00
Winston Chang
39a23af138 Merge pull request #3038 from rstudio/carson/bugfix/dateInput 2020-12-04 13:36:38 -06:00
Winston Chang
d8715819dc Build JS files 2020-12-04 13:27:51 -06:00
Carson
12444807e8 Better setting of bootstrap-datepicker start/end dates, closes #2703 2020-12-04 13:27:13 -06:00
Winston Chang
92077d47a1 Merge pull request #3042 from rstudio/carson/feature/aria-live 2020-12-04 13:08:03 -06:00
Winston Chang
4f54276e1b yarn build 2020-12-04 13:07:38 -06:00
Carson
ac30848019 Also default to aria-live='polite' when input bindings are about to receiveMessage 2020-12-04 13:07:11 -06:00
Carson
921650f53b When binding shiny outputs, have the 'aria-live' attribute default to 'polite', closes #2987 2020-12-04 13:07:11 -06:00
Winston Chang
72d81e8a85 Add label to private$currentThemeDependency 2020-12-04 12:32:55 -06:00
Carson Sievert
5c5974106d Properly attach jqueryui dependency when drag_drop plugin is used (#3185)
* Properly attach jqueryui dependency when drag_drop plugin is used, closes #3183

* write a unit test
2020-12-04 10:52:26 -06:00
Winston Chang
c2cbd3a127 Create session$currentThemeDependency only when needed (#3182)
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2020-12-04 09:47:18 -06:00
Winston Chang
8e5aedec00 Do correct assignment of resizeObserverCreated (#3177) 2020-12-04 08:58:52 -06:00
Winston Chang
13965acb37 getCurrentOutputInfo(): only create reactives when needed (#3180) 2020-12-03 17:16:33 -06:00
Winston Chang
8a99b9d401 Add label to reactive
Co-authored-by: Barret Schloerke <barret@rstudio.com>
2020-12-03 17:09:30 -06:00
Carson Sievert
f739a1d476 Set resolve = FALSE when getting thematic's bg option (#3178) 2020-12-03 17:02:12 -06:00
Winston Chang
87dd00be13 getCurrentOutputInfo(): only create reactives when needed 2020-12-03 16:15:04 -06:00
Winston Chang
8cd393597a Remove old note about Bootstrap 3 2020-12-03 09:09:27 -06:00
Winston Chang
b7366ef672 Remove outdated comments 2020-12-02 20:56:33 -06:00
Winston Chang
3d6329dee8 Add cacheWriteHook and cacheReadHook (#3173) 2020-12-02 16:17:08 -06:00
Winston Chang
2171420e0c Fix NA check 2020-12-02 16:03:07 -06:00
Winston Chang
e44a9b1ded Replace %OR% with %||% from rlang (#3172) 2020-12-02 12:14:07 -06:00
Winston Chang
bde5a88295 Fix test 2020-12-01 13:18:59 -06:00
Carson Sievert
11babd5567 Improve color contrasting in date and slider input (#3167)
* leverage bslib's color-contrast() in sliderInput()'s Sass and reduce number of git patches

* Use color-contrast() instead of color-yiq() in Bootstrap Datepicker
2020-11-25 11:39:00 -06:00
Carson Sievert
4c35d483bc Wait longer to disable old stylesheets (#3168) 2020-11-25 11:13:18 -06:00
Carson Sievert
d049558728 shinyAppTemplate()'s shinytest tests should be calling expect_pass() (#2943)
Co-authored-by: Winston Chang <winston@stdout.org>
2020-11-24 14:34:45 -06:00
Carson Sievert
8eed42387c Make sure dateInput() warns on empty string (#3165)
Co-authored-by: Winston Chang <winston@stdout.org>
2020-11-24 10:34:39 -06:00
Winston Chang
5b3366f35a Replace renderCachedPlot() code with bindCache() (#3163) 2020-11-23 14:34:52 -06:00
Winston Chang
fea7397c3b Merge pull request #3160 from rstudio/wch-bindevent-reactlog 2020-11-23 11:05:25 -06:00
runner
4a33582482 Document 2020-11-23 14:49:22 +00:00
Winston Chang
1bad0553b7 Fix labels for eventReactive 2020-11-20 15:46:18 -06:00
Winston Chang
ac0b723bb0 bindEvent(): alter observers in place and fix labels 2020-11-20 14:51:35 -06:00
Winston Chang
39454a6c09 Use enquos0, inject, and zap_srcref from rlang (#3157) 2020-11-20 11:07:06 -06:00
Winston Chang
569157aded Add reactive session$getCurrentTheme() (#3116)
Co-authored-by: Carson <cpsievert1@gmail.com>
2020-11-20 10:54:51 -06:00
Carson Sievert
d2d7770c76 require newest bslib version, follow up to #3155 (#3159) 2020-11-20 10:01:59 -06:00
Carson Sievert
5da846f1ce Always ship selectize HTML dependencies with name='selectize', closes #3125 (#3155) 2020-11-20 09:46:08 -06:00
Winston Chang
713c9ec923 Add render function quosure tests 2020-11-20 09:38:16 -06:00
Winston Chang
b3369616d2 bindCache: accept cache arg when used with renderPlot 2020-11-19 15:14:10 -06:00
Winston Chang
082b8ef080 bindCache: add documentation about renderPlot 2020-11-19 15:13:39 -06:00
Hadley Wickham
0fb9226a9b Minor bindCache doc tweaks (#3156) 2020-11-19 15:04:26 -06:00
Winston Chang
bb55f45d94 More bindCache documentation edits 2020-11-18 13:21:03 -06:00
Winston Chang
5b12980b7a Update bindEvent documentation 2020-11-18 12:39:20 -06:00
Winston Chang
493ef59dda Update bindCache documentation 2020-11-18 12:37:06 -06:00
Barret Schloerke
b42d835cbf Test visibility with latest promises (#3151)
Co-authored-by: Winston Chang <winston@stdout.org>
2020-11-18 11:36:28 -06:00
Winston Chang
d1d177f80f Merge pull request #3149 from rstudio/wch-plot-cache 2020-11-17 21:57:46 -06:00
Winston Chang
433e5814ed Add note about cleanup 2020-11-16 14:09:11 -06:00
Winston Chang
2bf9f42b49 Better cacheHint for renderPlot 2020-11-16 13:47:46 -06:00
Winston Chang
65efb573bd Hoist attributes when wrapping render functions 2020-11-16 13:12:27 -06:00
Winston Chang
26a701215d Cleaner way to pass in fitDims to renderPlot function 2020-11-16 12:08:22 -06:00
Winston Chang
3be7a20f40 Use enquos0 2020-11-16 11:00:27 -06:00
Winston Chang
6f8092f5a4 bindCache.shiny.renderPlot: Draw plot at size specified by sizePolicy() 2020-11-16 11:00:27 -06:00
Winston Chang
652fcfe799 First implementation of bindCache() for renderPlot() 2020-11-16 10:59:25 -06:00
Winston Chang
d7d03ee6a8 Convert ... to quosures 2020-11-16 10:59:25 -06:00
Winston Chang
dc6335ed4d Round pixelratio to two decimals (#3147) 2020-11-16 10:06:38 -06:00
Winston Chang
b421f6bd7f Destroy correct object. Closes #3145 2020-11-13 19:30:13 -06:00
Winston Chang
d4358e0793 Fix renderCachedPlot expression handling 2020-11-13 17:53:55 -06:00
Winston Chang
a8dfa0771f Merge pull request #3144 from rstudio/wch-non-quosure 2020-11-13 17:08:41 -06:00
Winston Chang
6df3ce4b19 Rename blast() to inject() 2020-11-13 15:13:33 -06:00
Winston Chang
8f40f8cab8 Add additional quosure tests 2020-11-13 15:11:18 -06:00
Winston Chang
0d5a2cee58 Use brackets
Co-authored-by: Barret Schloerke <barret@rstudio.com>
2020-11-13 15:04:11 -06:00
Winston Chang
8db4f41fa9 Update bindCache documentation 2020-11-13 15:03:17 -06:00
Winston Chang
b85b03583b Add cache hint tests 2020-11-13 14:56:25 -06:00
Winston Chang
28e18fe87b Update examples 2020-11-13 13:45:48 -06:00
Dean Attali
2c1961acd7 added ... support to icon(); fixes #3140 (#3143)
Co-authored-by: Winston Chang <winston@stdout.org>
2020-11-13 13:25:03 -06:00
Winston Chang
04386f1a5e reactivePoll: store environment 2020-11-13 13:11:35 -06:00
Winston Chang
9c915e52ca Add get_quosure function 2020-11-12 23:45:23 -06:00
Winston Chang
6b6ab48377 Don't unwrap quosures for key or event expressions 2020-11-12 22:16:00 -06:00
Winston Chang
bf36d07670 Add tests for quosures in reactive() and observe() 2020-11-12 18:17:02 -06:00
Winston Chang
7166192143 Bump rlang version dependency 2020-11-12 17:57:04 -06:00
Winston Chang
509f0790db Reactive functions don't unwrap quosures automatically 2020-11-12 17:10:49 -06:00
Winston Chang
67a776a39a Don’t automatically unwrap quosures 2020-11-12 16:57:05 -06:00
Winston Chang
d3701df4e6 Safer method for remove_source 2020-11-12 16:57:05 -06:00
Carson Sievert
0195e34a7b pre.shiny-text-output shouldn't set border-radius by default (#3142) 2020-11-12 15:31:04 -06:00
Winston Chang
0aa49c8a93 Rebuild shiny.js 2020-11-10 14:16:43 -06:00
Winston Chang
437de58922 Remove source refs for nested functions 2020-11-09 16:23:28 -06:00
Winston Chang
fc76cf21fb Merge pull request #3127 from rstudio/with-cache-event 2020-11-09 13:31:08 -06:00
Winston Chang
23d1b25c46 Bump version to 1.5.0.9005 2020-11-09 13:16:02 -06:00
Winston Chang
8bfb59875f Temporarily disable deprecatedEnvQuotedMessage 2020-11-09 13:10:51 -06:00
Winston Chang
36e866743d Documentation fixes 2020-11-09 11:00:13 -06:00
Winston Chang
d35c6e35ce Use is_false function 2020-11-09 10:55:17 -06:00
Winston Chang
e9afd8c99e bindCache documentation updates 2020-11-09 10:52:19 -06:00
Winston Chang
43b7c41c4f Rename withCache and withEvent to bindCache and bindEvent 2020-11-08 21:47:11 -06:00
Winston Chang
921f60475e Update withEvent help 2020-11-06 15:33:01 -06:00
Winston Chang
58433cda01 Add tests for ignoreNULL 2020-11-06 14:20:20 -06:00
Winston Chang
ed5eca5496 withEvent: invoke render function with ... 2020-11-06 14:19:59 -06:00
Winston Chang
eff4a1f23e Add cache hint to reactive expressions 2020-11-06 12:41:57 -06:00
Carson Sievert
9f72b15fcf Resend CSS info (via bindAll) when a restyle (i.e., setCurrentTheme()) happens, closes #3119 (#3134) 2020-11-05 18:03:21 -06:00
Winston Chang
8069ff2b05 Fixes for tests 2020-11-05 17:02:56 -06:00
Winston Chang
10deddf2f0 Export quoToFunction 2020-11-05 16:57:35 -06:00
Winston Chang
3ad1c4076d Make renderCachedPlot accept quosures 2020-11-05 16:37:48 -06:00
Winston Chang
943f31e117 Fix cacheHint=FALSE 2020-11-05 16:37:18 -06:00
Winston Chang
c43bc195e7 Use cacheHint 2020-11-05 14:25:19 -06:00
Winston Chang
92b1e8f256 Fixes for withCache and render functions 2020-11-05 09:20:23 -06:00
Winston Chang
985970d320 Include user expression in cache key for render functions 2020-11-04 18:07:26 -06:00
Winston Chang
5eabaa5207 Allow render functions to accept quosures 2020-11-04 18:07:08 -06:00
Carson Sievert
7f60ecc725 Have slider's fg/bg defaults fallback to body colors if input colors are transparent, closes #3130 (#3131)
* Have slider's fg/bg defaults fallback to body colors if input colors are transparent, closes #3130

* Add comment

* Rename
2020-11-04 17:59:02 -06:00
Winston Chang
7c635e1283 Remove unused function 2020-11-04 12:53:50 -06:00
Carson Sievert
4727a7adf4 rename bootstraplib package to bslib (#3132) 2020-11-04 11:50:55 -06:00
Winston Chang
8940f14dde Fixes for R CMD check 2020-11-03 14:59:08 -06:00
Winston Chang
2fd0ce1a09 Add withEvent observer tests 2020-11-03 13:35:08 -06:00
Winston Chang
638bcc0f85 Mark some render functions as uncacheable 2020-11-03 13:12:23 -06:00
Winston Chang
d411da3114 Fix variable names 2020-11-03 13:04:11 -06:00
Winston Chang
0acae46835 Document withCache 2020-11-03 13:04:10 -06:00
Winston Chang
61cc61d9aa Add methods for withEvent and withCache 2020-11-03 13:03:35 -06:00
Winston Chang
194320d163 Remove cachedReactive() 2020-11-03 13:03:33 -06:00
Winston Chang
902bfb8628 Convert observeEvent and eventReactive to use withEvent 2020-11-03 13:02:57 -06:00
Winston Chang
b25d72f698 Streamline reactive() and observe() 2020-11-03 13:02:57 -06:00
Winston Chang
a4d8f541dd Fix test 2020-11-03 13:02:57 -06:00
Winston Chang
6aaf2ff4d5 Add withEvent.Observer 2020-11-03 13:02:57 -06:00
Winston Chang
b0f77d6591 Store original function as attribute on wrapper 2020-11-03 13:02:57 -06:00
Winston Chang
f2885dafd2 Add functions for extracting parts out of reactive expressions 2020-11-03 13:02:57 -06:00
Winston Chang
b0725e0153 Add withCache and withEvent functions 2020-11-03 13:02:49 -06:00
Winston Chang
4ce62034ce Convert reactive() and observe() to accept quosures 2020-11-03 13:01:27 -06:00
Winston Chang
7d4c0ad611 Remove extra argument 2020-11-03 13:01:17 -06:00
Winston Chang
d189cd9f23 Use cache objects from cachem package (#3118)
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2020-11-03 12:01:17 -06:00
Winston Chang
f61ba70bb9 Merge pull request #3094 from rstudio/reactive-cached 2020-10-29 11:26:26 -05:00
Winston Chang
6e48692637 More code review changes 2020-10-29 11:13:22 -05:00
Winston Chang
f7b1bc0e5c Move key after ... 2020-10-29 10:19:16 -05:00
Winston Chang
a213d6f7e1 Increase size of diskCache 2020-10-29 10:16:07 -05:00
Winston Chang
a7d793ecf9 Code review feedback 2020-10-29 10:13:39 -05:00
Winston Chang
5d25481f66 Add remove_srcref for R<3.6 2020-10-29 10:13:10 -05:00
Winston Chang
77a8a783de cachedReactive: add tests for quosures 2020-10-28 20:11:03 -05:00
Winston Chang
0492eb7958 Update cachedReactive docs about cache collisions 2020-10-28 19:28:06 -05:00
Winston Chang
d37feea299 Rename args for cachedReactive 2020-10-28 19:16:12 -05:00
Winston Chang
ffb9ad2094 Merge pull request #3113 from rstudio/wch-fix-options-modules 2020-10-28 15:22:02 -05:00
Winston Chang
1e63dfc4c5 Bump recursion depth to 20 2020-10-28 15:15:12 -05:00
Winston Chang
051cc51d4b Handle shinyOptions in nested modules 2020-10-28 13:58:17 -05:00
Carson Sievert
56dd92fee8 Register all theme dependencies to be processes since _new_ HTML dependencies may be added to the theme object (#3115) 2020-10-28 13:58:07 -05:00
Winston Chang
51b835b57f Merge pull request #3114 from rstudio/wch-warn-package 2020-10-27 21:19:40 -05:00
Winston Chang
ccd7342986 Check for NAMESPACE file 2020-10-27 15:33:23 -05:00
Winston Chang
82decaa070 Fix use of shinyOptions in modules 2020-10-27 15:15:23 -05:00
Winston Chang
d1e808d090 Update NEWS 2020-10-27 15:12:27 -05:00
Winston Chang
7aad389338 Warn when running app in a R package directory 2020-10-27 15:12:24 -05:00
Winston Chang
7e07c460de Rebuild shiny.min.css 2020-10-27 14:07:24 -05:00
Carson Sievert
81a8ec3ce1 pre.shiny-text-output should have a border-radius set by default (#3111) 2020-10-23 13:20:25 -05:00
Carson Sievert
800f0a216d Get session$setCurrentTheme() working on IE11 & improve timing of disabling old stylesheets (#3097)
* disable stylesheet on next tick to avoid FOUC

* fix regex matching logic

* Avoid regex and remove stylesheet's ownerNode after disabling

* Use inline <style> as opposed to <link> tags when restyling for IE11 compatibility

* Be more careful to avoid possibility of removal modifying styleSheets

* Use inline <style> for IE; otherwise update the <link>

* Update srcjs/output_binding_html.js

Co-authored-by: Winston Chang <winston@stdout.org>

* Update browser.js to correctly detect IE11

* remove redundant if statements

Co-authored-by: Winston Chang <winston@stdout.org>
2020-10-23 11:06:42 -05:00
Winston Chang
dade7dc069 Increase default memory and disk cache size 2020-10-23 00:38:20 -05:00
Winston Chang
b271d0a9a2 Add valueExpr to cache key 2020-10-23 00:36:18 -05:00
Winston Chang
5daa0bc38e Handle future objects 2020-10-22 20:06:03 -05:00
Winston Chang
22665dc9b4 cachedReactive: add label arg 2020-10-22 16:00:41 -05:00
Winston Chang
a99f11fb10 Extract reactive label code into exprToLabel() 2020-10-22 15:58:50 -05:00
Winston Chang
81824575e6 Use consistent interface for preserving visibility 2020-10-22 00:01:32 -05:00
Winston Chang
f6d010056a Skip visibility tests with old versions of rlang 2020-10-21 23:50:24 -05:00
Winston Chang
ffd20bcc6e hybrid_chain: preserve visibility 2020-10-21 23:50:24 -05:00
Winston Chang
55eaaa869d Cache errors and visibility 2020-10-21 18:22:33 -05:00
Winston Chang
c2e66ca474 Split setVisible into two functions 2020-10-21 14:57:02 -05:00
Winston Chang
62b848c2e2 Throw error if no cache object 2020-10-21 14:55:48 -05:00
Winston Chang
dbb657bd91 Merge pull request #3091 from rstudio/carson/feature/border-radius 2020-10-21 09:30:50 -05:00
Winston Chang
de871b79b0 Use ellipsis to check for empty args 2020-10-20 21:19:14 -05:00
Winston Chang
146a6d459d Rebuild docs 2020-10-20 21:18:58 -05:00
Winston Chang
9fb1dd18a7 Mention quosures 2020-10-20 21:11:04 -05:00
Winston Chang
9ae894d9e3 Use spookyhash 2020-10-20 21:08:27 -05:00
Winston Chang
56e0fbdb05 Update tests for cachedReactive 2020-10-20 21:04:40 -05:00
Winston Chang
e6325629a9 Add eventExpr to cachedReactive() 2020-10-20 21:04:03 -05:00
Carson
9a3329acc7 yarn build 2020-10-20 18:13:55 -05:00
Carson
75ab225d84 Make sure shiny.scss can be compiled without Bootstrap 2020-10-20 18:09:41 -05:00
Carson
883668ac93 Use Bootstrap's border-radius mixin (mainly so BS4's enable-rounded works as expected) 2020-10-20 18:06:56 -05:00
Winston Chang
c5f2dece49 Specify UTF-8 encoding for error page
This supersedes PR #3039
2020-10-20 14:35:04 -05:00
Winston Chang
b55bc5318e Add cachedReactive example with actionButton 2020-10-16 15:59:40 -05:00
Winston Chang
a39450c2b2 Add tests for cachedReactive error handling 2020-10-16 15:26:58 -05:00
Winston Chang
b784068701 More documentation for cachedReactive 2020-10-15 18:58:41 -05:00
Winston Chang
bac4e68b89 Add support for async in cachedReactives 2020-10-15 17:19:34 -05:00
Winston Chang
20e95a4cab Add tests for cachedReactive 2020-10-15 17:19:34 -05:00
Winston Chang
96da457db3 Add cachedReactive 2020-10-15 17:19:34 -05:00
Winston Chang
37b8715cff Move resolve_cache_object out of renderCachedPlot 2020-10-15 17:19:34 -05:00
Barret Schloerke
7aa3a243ba Make template test less brittle and failures more verbose (#3096)
Also updated a pkgdown check
2020-10-15 11:32:50 -04:00
Winston Chang
f2b549f9cd Merge pull request #3095 from rstudio/get_option_null 2020-10-14 15:55:46 -05:00
Barret Schloerke
bc58dba0ad Add list default to getShinyOption("themeDependencyFuncs") call
Does NOT work on R <= v3.6.3

```r
x <- NULL
x[[length(x) + 1]] <- identity
```
2020-10-14 16:24:22 -04:00
Barret Schloerke
8ef9be5290 Explicitly state the default value in getShinyOption(x, default) calls 2020-10-14 16:23:07 -04:00
Winston Chang
d95560db09 Merge pull request #3085 from rstudio/carson/feature/restyle 2020-10-14 10:17:51 -05:00
Winston Chang
98f64df738 Dependency fixes 2020-10-14 10:06:42 -05:00
Winston Chang
8634e372da bs_dependency_dynamic -> bs_dependency_defer 2020-10-14 09:42:16 -05:00
Winston Chang
cbda7633e0 Update MockShinySession 2020-10-13 16:00:15 -05:00
Winston Chang
8f00cf50ca Fix typo 2020-10-13 15:57:52 -05:00
Winston Chang
acca28075b Merge branch 'master' into carson/feature/restyle
Also rebuild shiny.js
2020-10-13 15:56:53 -05:00
Winston Chang
9a563463dd Add namespacing for bs_dependency() 2020-10-13 15:32:00 -05:00
Winston Chang
1ede94b09e Remove registerThemeDependency from pkgdown 2020-10-13 15:30:56 -05:00
Winston Chang
7f41a54c4e Move bootstraplib to Imports 2020-10-13 14:49:51 -05:00
Winston Chang
6cfab79ce9 Make setCurrentTheme() a ShinySession method 2020-10-13 14:47:40 -05:00
Carson Sievert
f33b3c7eef Update R/bootstrap.R 2020-10-13 14:15:12 -05:00
Carson
93d78ae2b0 prevent caching on restyle and account for fact that href may include hostname 2020-10-13 12:37:02 -05:00
Winston Chang
c498b02289 textAreaInput() doesn't work as expected for relative width (#2049)
Squashed commit of the following:

commit a823dd5d7da6fafba69f783e112d71d9dcd09c5f
Author: Winston Chang <winston@stdout.org>
Date:   Mon Oct 12 12:59:41 2020 -0500

    Remove trailing whitespace

commit ae55b519fb0f7d97f559e2f487063366926aa41d
Author: Winston Chang <winston@stdout.org>
Date:   Mon Oct 12 12:52:04 2020 -0500

    Move NEWS item to correct location

commit aa89abc247be1bf5cdf093ff7fe7c51711821438
Merge: ee98773f a1ff7652
Author: Winston Chang <winston@stdout.org>
Date:   Mon Oct 12 12:47:21 2020 -0500

    Merge branch 'master' into shrektan-textarea-style

commit ee98773f1d
Author: shrektan <shrektan@126.com>
Date:   Sat Aug 24 10:35:23 2019 +0800

    correct the NEWS entry

commit b468d8f013
Author: shrektan <shrektan@126.com>
Date:   Sat Aug 24 10:32:52 2019 +0800

    the width of textarea should be specified in the parent div.

commit 5abdcf9260
Merge: 03079f0a b07e553b
Author: shrektan <shrektan@126.com>
Date:   Sat Aug 24 09:54:27 2019 +0800

    resolve conflicts

    Merge remote-tracking branch 'origin/master' into textarea-style

    # Conflicts:
    #	R/input-textarea.R

commit 03079f0a14
Author: shrektan <shrektan@126.com>
Date:   Mon May 7 16:08:05 2018 +0800

    Fixed a bug that `textAreaInput()` doesn't work as expected for relative `width`.
2020-10-12 13:01:44 -05:00
Winston Chang
30b62e6f18 Update bootstrap dependency code 2020-10-09 17:57:50 -05:00
Barret Schloerke
ec18ef651b Add missing space for html lang (#3093)
Followup from https://github.com/rstudio/shiny/pull/3087
2020-10-08 11:02:55 -04:00
Joe Cheng
aad23686fa Bump version number 2020-10-07 16:43:28 -07:00
Joe Cheng
1c85ecd7c0 Bump development version 2020-10-07 16:20:32 -07:00
Winston Chang
a1ff765235 Merge pull request #3055 from rstudio/joe/bugfix/freeze-invalidation 2020-10-07 17:36:16 -05:00
Hadley Wickham
a30ba9226d Implement reactiveConsole() (#3092)
* Implement reactiveConsole(). Fixes #2518

* Also includes makeReactiveBinding tweaks
  * use `reactiveConsole()` to provide a nicer example
  * simplified the implementation using `reactiveVal()`
  * remove from documentation indexes since you probably don't want to be promoting this function
2020-10-07 15:37:14 -05:00
Joe Cheng
980a1e53a7 tabsetPanel binding: unconditionally trigger change on receiveMessage
This brings it into line with all of the other input bindings.
The only exception is sliderInput, which has a more complicated
codepath that goes out of its way to force the slider, for its
own reasons; I didn't change the slider for fear of breaking
something, and it also doesn't exhibit the problem I'm here to
fix (next paragraph).

The goal is to ensure that if forgetLastInput is called on an
input, and then that input receives a message (updateXXXInput)
to update its value, BUT the new value is the SAME as its
existing value, that the input binding still acts like something
changed. This is because we need the id/value to go through
the InputSender code path, and alert the server if a previously
frozen input is now thawed.
2020-10-06 14:30:21 -07:00
Joe Cheng
00092cd2a8 NEWS 2020-10-06 14:29:29 -07:00
Joe Cheng
53ddb54936 Forgot to actually force invalidation for freezeReactiveValue(input) 2020-10-06 14:28:50 -07:00
Joe Cheng
ea1e307a51 Preserve existing (CRAN) behavior of freezeReactiveVal/freezeReactiveValues(non-input), but warn
We don't think anyone is using the freeze functions in the ways
that we are deprecating, if so they should contact us via the
link provided.

If it turns out nobody complains, we can remove the problematic
functions. If people complain, then we'll find out what they're
using them for and we can fix them properly.
2020-10-06 14:28:49 -07:00
Joe Cheng
17bc1e2e06 Force invalidation on freeze 2020-10-06 14:28:49 -07:00
Joe Cheng
ed8f3b730b Address ("fix" is too strong a word) #1791, #2946: freeze/thaw
1. freezeReactiveValue(input, "x") is called, inside a renderUI
   or in an observer that then calls updateXXXInput
2. Some reactive output tries to access input$x, this takes a
   reactive dependency but throws a (silent) error
3. When the flush cycle ends, it automatically thaws

What's *supposed* to happen next is the client receives the new
UI or updateXXXInput message, which causes input$x to change,
which causes the reactive output to invalidate and re-run, this
time without input$x being frozen.

This works, except when the renderUI or updateXXXInput just so
happens to set input$x to the same value it already is. In this
case, the client would detect the duplicate value and not send
it to the server. Therefore, the reactive output would not be
invalidated, and effectively be "stalled" until the next time it
is invalidated for some other reason.

With this change, freezeReactiveValue(input, "x") has a new side
effect, which is telling the client that the very next update to
input$x should not undergo duplicate checking.
2020-10-06 14:28:49 -07:00
Carson
0e109d5237 shiny.css should also be using bs_runtime_dependencies() 2020-10-05 16:21:14 -05:00
Winston Chang
f672226a3d Merge pull request #3087 from rstudio/jooyoungseo-lang-a11y 2020-10-05 15:42:26 -05:00
Carson
481dccd085 Don't export setCurrentTheme() (just provide it as a public session method) 2020-10-05 15:21:59 -05:00
Barret Schloerke
9612f1c3c8 Use a for loop over an lapply to get better error reporting 2020-10-05 15:56:27 -04:00
Barret Schloerke
9e1e5f61a3 Use inline template logic 2020-10-05 15:48:51 -04:00
Barret Schloerke
99a566f473 Tell htmltools that the template is a complete template 2020-10-05 15:17:27 -04:00
Barret Schloerke
1012307467 Make sure lang is set in helper method 2020-10-05 15:16:57 -04:00
Barret Schloerke
b729f45eaf Add missing assignment from merge w/ master 2020-10-05 14:36:11 -04:00
Barret Schloerke
cab799e6ee Add missing close bracket 2020-10-05 14:34:51 -04:00
Carson
a06322d155 No need to capture options since the options are set at render-time 2020-10-05 13:23:59 -05:00
Barret Schloerke
d836cb2a2c Use helper function to cleanly create <html> start tag given the lang val 2020-10-05 12:14:05 -04:00
Barret Schloerke
2249c7a28a Use @inheritParams bootstrapPage to inherit lang definition 2020-10-05 11:58:21 -04:00
Barret Schloerke
0d0422c0a7 Use lang <- getLang(ui) and ui <- setLang(ui, lang) methods 2020-10-05 11:57:17 -04:00
Barret Schloerke
5ea556ee77 Fix docs bug 2020-10-05 11:50:16 -04:00
Barret Schloerke
a34496663b Merge branch 'lang-a11y' of https://github.com/jooyoungseo/shiny into jooyoungseo-lang-a11y 2020-10-05 11:48:00 -04:00
Carson
abeaa71d8d wip generalized real-time theming via setCurrentTheme() 2020-10-02 17:32:57 -05:00
Carson
281a427718 Allow Shiny.renderDependencies() to re-render stylesheets, if requested
Also, make getCurrentTheme() aware of the reactive theme set by bootstraplib::bs_themer()
2020-10-02 11:13:19 -05:00
Carson Sievert
03ab966cdc Make shiny.css bootstraplib aware; use jquerylib (#3060)
* Sassify shiny.css (& make it themable); gut json2 dependency (was there for IE8 support)

* Always serve a compressed bundle; remove shiny.css

* Use getCurrentTheme() and make sure shinyDependencies is a function

* Make sure we have sass/rprojroot before running checkBuilt.sh

* Need repos set

* Compare against bleeding edge of sass

* Perform built check with testthat (copying the approach taken for pkgdown checks)

* Update tests/testthat/test-built-files.R

Co-authored-by: Winston Chang <winston@stdout.org>

Co-authored-by: Winston Chang <winston@stdout.org>
2020-10-02 10:59:59 -05:00
Winston Chang
3dcb810346 Merge pull request #3083 from rstudio/is-bs-theme 2020-10-01 14:17:18 -05:00
Carson
d6eef8b3e1 Add getCurrentTheme to pkgdown 2020-10-01 13:47:59 -05:00
Carson
a770f1cbf2 yarn build 2020-10-01 13:40:50 -05:00
Carson
a5687df9b4 make sure is_bs_theme() is available 2020-10-01 13:33:49 -05:00
Carson Sievert
202881cbbd Localized bootstraplib themes (#3062)
* Get rid of the bootstraplib option and simply use bootstraplib when a theme is active

* Restore previous bootstraplib theme when exiting a file/dir based runApp

* wip use latest htmltools+bootstraplib to sketch out local theme API

* Don't do anything with bootstraplib's global state and make sure bs_theme is an expected value

* typo

* better docs

* bugfix

* Use the new, more general, tagFunction() instead

* Set the theme object as a part of the page layout (instead of in shinyApp())

* rollback the structural changes to selectizeIt() to avoid breaking code that makes assumptions about the return value of selectInput()

* set shinyOption() in bootstrapLib(), not bootstrapPage()

* Add a helper for checking whether theme is a bs_theme

* Make theme a required arg in bootstrapSass

* Have bootstrapLib() call shinyOption() at render-time, and document why it works

* Have bootstrapPage() always place bootstrapLib(theme) at the top of the tagList()

* Only set shinyOption() when an application is running at render-time (otherwise; throw a warning)

* code review

* Export a new getCurrentTheme() for Shiny developers to access the theme's Sass code

* bump version
2020-10-01 13:19:18 -05:00
Winston Chang
24ac3b9d8b Reogranize shinyOptions documentation 2020-09-30 15:55:27 -05:00
Winston Chang
47c1fb88b9 Merge pull request #3080 from rstudio/wch-shinyoptions-session 2020-09-29 10:54:14 -05:00
Winston Chang
170b143b17 Document cache option, remove bootstraplib 2020-09-29 10:44:49 -05:00
Winston Chang
3854b49c35 Document shiny.autoload.r option 2020-09-29 10:37:08 -05:00
Winston Chang
bae4f604b5 Update NEWS 2020-09-29 10:34:43 -05:00
Winston Chang
36f32e14d3 Add shinyOptions scoping tests 2020-09-28 19:15:20 -05:00
Winston Chang
9e521e6927 Add missing paren 2020-09-28 17:44:05 -05:00
Winston Chang
4176f541fc Add test for captureAppOptions()
Co-authored-by: Barret Schloerke <barret@rstudio.com>
2020-09-28 17:40:34 -05:00
Winston Chang
18f2afbf85 Fix missing itemize 2020-09-28 16:34:16 -05:00
Winston Chang
f9a94d9758 Merge pull request #3075 from rstudio/wch-rm-ie-upload 2020-09-28 16:03:51 -05:00
Winston Chang
d1e672e3e3 Remove $saveFileUrl method 2020-09-28 16:03:19 -05:00
Winston Chang
202b924e63 Separate docs for options and shinyOptions 2020-09-28 10:43:08 -05:00
Winston Chang
4c3342aa99 Get rid of .globals$testMode 2020-09-25 14:44:59 -05:00
Winston Chang
b1e5dd1d1d Remove .globals 2020-09-25 14:16:10 -05:00
Winston Chang
d43ebfbdb9 Remove withLocalOptions 2020-09-25 14:15:34 -05:00
Winston Chang
cfadd8307b Copy app and session-level shinyOptions at instantiation time 2020-09-25 14:06:36 -05:00
Winston Chang
fa6cf9832d Add session-level shinyOptions 2020-09-24 20:55:17 -05:00
Winston Chang
0075b0da33 Annotate runApp and use initCurrentAppState() 2020-09-24 20:35:03 -05:00
Winston Chang
59c6367cb5 Use getCurrentAppState() for shinyOptions instead of .global$options 2020-09-24 20:33:35 -05:00
Winston Chang
194323a9ee Removed useless option setting
This option is immediately cleared by the next line of code.
2020-09-24 19:52:10 -05:00
Winston Chang
1ca437e4ee Add tests for captureAppOptions 2020-09-24 19:48:10 -05:00
Winston Chang
77e43b9f50 Rename consumeAppOptions to captureAppOptions and provide example 2020-09-24 19:47:48 -05:00
Winston Chang
a23f4b0224 Merge pull request #3078 from rstudio/wch-rename1 2020-09-24 19:03:23 -05:00
Winston Chang
0541c90980 Re-document 2020-09-24 14:53:38 -05:00
Winston Chang
b73a263de8 Rename server-orig.R back to server.R 2020-09-24 14:47:01 -05:00
Winston Chang
d7ffee68cf Merge branch 'wch-rename' into wch-rename1 2020-09-24 14:46:30 -05:00
Winston Chang
89cd42b904 Create server-resource-paths.R from server.R 2020-09-24 14:45:37 -05:00
Winston Chang
404185eb8c Merge branch 'wch-rename' into wch-rename1 2020-09-24 14:44:47 -05:00
Winston Chang
8c29a81b12 Create runapp.R from server.R 2020-09-24 14:43:46 -05:00
Winston Chang
9b42c6c379 Merge branch 'wch-rename' into wch-rename1 2020-09-24 14:42:48 -05:00
Winston Chang
03c2dd9e4b Rename server.R server-orig.R 2020-09-24 14:40:09 -05:00
Winston Chang
d8274c3d8c Create viewer.R from server.R 2020-09-24 14:37:25 -05:00
Winston Chang
85b5fb090f Add back shinyapp.R 2020-09-24 14:32:20 -05:00
Winston Chang
2adef311ed Merge branch 'wch-rename-fix' into main 2020-09-24 14:31:17 -05:00
Winston Chang
7050d0b8ad Remove shinyapp.R 2020-09-24 14:30:42 -05:00
Winston Chang
8358144a4f Rename file 2020-09-24 14:28:15 -05:00
Winston Chang
44e083e0a9 Re-document 2020-09-24 12:58:06 -05:00
Winston Chang
a5418cf6ee Rename app.R to shinyapp.R 2020-09-24 12:57:27 -05:00
Winston Chang
c74630d6eb Split knitr.R from app.R 2020-09-24 12:55:53 -05:00
Winston Chang
157d4ac9a9 Split knitr.R from app.R 2020-09-24 12:48:32 -05:00
Winston Chang
8228613c01 Create knitr.R from app.R 2020-09-24 12:47:21 -05:00
Winston Chang
b907e17b70 Rebuild shiny.js 2020-09-24 10:49:13 -05:00
Winston Chang
aa7000427c Update NEWS 2020-09-24 10:48:57 -05:00
Winston Chang
8562c90454 Remove allowDataUriScheme because it is no longer needed 2020-09-24 10:47:27 -05:00
Winston Chang
5a9a04cd72 Remove IE8 and IE9 file upload support 2020-09-24 10:47:27 -05:00
Winston Chang
6b32611356 Rebuild shiny.js 2020-09-24 10:41:01 -05:00
colin
770ebc394f updateRadioButton with character(0) (#3043)
Squashed commit of the following:

commit a095d75b67a0bad439e8d6c495ef81af25c0b1a9
Author: Winston Chang <winston@stdout.org>
Date:   Thu Sep 24 10:30:47 2020 -0500

    Update NEWS

commit 715a10ebd63c34eb2f464a7388e0b89b994bee0f
Author: Winston Chang <winston@stdout.org>
Date:   Thu Sep 24 10:24:24 2020 -0500

    Update docs for radioButtons about having none selected

commit eff9036884693002a84f84df16cf699be2358c1c
Author: Winston Chang <winston@stdout.org>
Date:   Thu Sep 24 10:15:59 2020 -0500

    Cleaner check for no selected radioButtons

commit 1666baa746f4dea986be4929720de2a5653acbb6
Merge: c0d35e84 b04ba393
Author: Winston Chang <winston@stdout.org>
Date:   Thu Sep 24 10:02:16 2020 -0500

    Merge branch '2688' of https://github.com/ColinFay/shiny into ColinFay-2688

commit b04ba393b8
Author: colin <colin@thinkr.fr>
Date:   Thu Sep 24 08:37:58 2020 +0200

    changed the test structure

commit 866a86946a
Author: colin <colin@thinkr.fr>
Date:   Sun Sep 13 20:50:44 2020 +0200

    restore old `$escape` behavior

commit d45af353fd
Author: colin <colin@thinkr.fr>
Date:   Fri Sep 11 08:54:25 2020 +0200

    added trailing ; and space before {

commit 5e95ee03a1
Author: colin <colin@thinkr.fr>
Date:   Thu Sep 10 21:55:02 2020 +0200

    return early if the value is undefined in setValue of radio

commit 24ac6ec624
Author: colin <colin@thinkr.fr>
Date:   Thu Sep 10 21:53:41 2020 +0200

    Testing that the type of val is a string, instead of relying on the length

commit 18ec3b8540
Author: colin <colin@thinkr.fr>
Date:   Wed Sep 9 22:08:45 2020 +0200

    Radio buttons can now be reset with character(0), and their value is set to NULL

    Close #2688 and close #2266

commit d7f66165d0
Author: colin <colin@thinkr.fr>
Date:   Wed Sep 9 22:08:06 2020 +0200

    Correct bug when $escape received an empty value
2020-09-24 10:38:12 -05:00
Joe Cheng
c0d35e84b1 Allow setting the state of RestoreContext (#3053)
* Allow setting the state of RestoreContext

This setter only sets public fields, but it's still necessary because
the RestoreInputSet R6 class is not exported.

(I needed this functionality for shinytableau config dialogs to do a
bookmark-like restore, but not at all based on querystring)

* Use list instead of values to prevent accidental mutation

See discussion here:
https://github.com/rstudio/shiny/pull/3053#discussion_r488948453

* Rebuild JS for new version number
2020-09-21 13:27:59 -07:00
Andrew Baxter
5e74478864 Remove test for date object length>1 (Closes #2936) (#3061)
Co-authored-by: Winston Chang <winston@stdout.org>
2020-09-16 17:11:06 -05:00
Winston Chang
46852e2051 Include sources in source maps
Uglify changed the option to include sources; this fix reinstates them.
2020-09-15 15:48:55 -05:00
Winston Chang
b9dded0bef Rebuild JS assets 2020-09-15 15:37:03 -05:00
Winston Chang
6d05f403a5 yarn upgrade 2020-09-15 15:34:47 -05:00
Winston Chang
8368634f85 Merge pull request #2959 from rstudio/joe/feature/select-choices-warning 2020-09-15 15:21:58 -05:00
Winston Chang
8d57d909b4 Merge branch 'master' into joe/feature/select-choices-warning 2020-09-15 15:21:45 -05:00
Winston Chang
9b7855d597 Add more authors 2020-09-15 15:13:42 -05:00
Winston Chang
6a5e1b9998 Add Barret to Authors 2020-09-15 13:15:04 -05:00
Carson Sievert
af6e558699 Make sure we only include one version of Bootstrap (#3048)
* Make sure we only include one version of Bootstrap and throw a warning if both bootstraplib and theme file is provided

* code review
2020-09-11 17:52:08 -05:00
Carson Sievert
26d4dddffd Remove modalButton() from pkgdown reference (it's not included on modalDialog()) (#3046) 2020-09-11 15:04:53 -05:00
Hadley Wickham
e2765b4881 Document modalButton with modalDialog (#2907) 2020-09-11 08:49:21 -05:00
Hadley Wickham
9796b25f33 Combine point events into one doc file (#2906) 2020-09-10 17:47:45 -05:00
Carson Sievert
01b8d3a314 More themable dateInput()/dateRangeInput() (#2964)
* upgrade bootstrap-datepicker from 1.6.4 to 1.9.0; setup infrastructure for bootstraplib theming

Note also that the 000 patch is no longer relevant as 1.9.0 includes the same fix https://github.com/uxsolutions/bootstrap-datepicker/pull/2009

* Patch sass code for BS4 support and more general color contrasting

* Wrap sass compilation into reusable function

* remove check warning

* Have bootstrapPage() use bootstraplib

* yarn build

* Use new output_template()

* Deprecate bootstrapLib() in favor of bootstraplib::bootstrap()

* Require bootstraplib 0.1.0.9001

* Sync up DESCRIPTION

* document

* rollback changes to pkgdown
2020-09-09 15:20:59 -05:00
Carson Sievert
50c48de0de More themable selectInput() (#2950)
* Add option to use bootstraplib and have selectInput() theming variables

* Use getShinyOption()

* Have useBsTheme() error out with informative messages if a theme isn't active

* Better Sass variables defaults that account for both bootswatch themes and bs_theme_base_colors()

* Add Carson as an author

* Provide better BS3/BS4 selectize sass variable defaults as a patch to the source files

* tidy up tools script

* add sass to suggests

* yarn install && yarn build

* Wrap sass compilation into reusable function

* Bring -color-item closer to -color-input

* Leverage the new sass::output_file()

* naming change

* Sync up with other PRs

* Sync up DESCRIPTION

* Forgot to update CSS output file logic

* document
2020-09-09 15:02:05 -05:00
Carson Sievert
bfc90da054 More themable sliderInput() (#2958)
* More themable sliderInput()

* Slider's accent color default should derive from primary theme color

* Remove custom theming args (skin, accentColor, sassVars) but still support theming via bootstraplib

* Wrap sass compilation into reusable function

* remove check warning

* Make font-family configurable; auto-contrast fromto handle text color; increase color contrasting (for accessbility)

* Sync up with other PRs

* Sync up DESCRIPTION

* Forgot to update CSS output file logic

* document

* code review
2020-09-09 14:38:10 -05:00
JooYoung Seo
9d8d6fd6b1 Resolve NEWS conflict 2020-09-09 15:17:25 -04:00
Winston Chang
43344d9a78 Merge pull request #3009 from jooyoungseo/landmark-a11y
Added a11y semantic landmarks for main and sidebar panels
2020-09-09 13:33:04 -05:00
Winston Chang
01a593c857 Rebuild docs 2020-09-09 13:14:39 -05:00
Winston Chang
1b2dd11a4c Merge pull request #2944 from daattali/patch-2 2020-09-09 13:12:36 -05:00
Winston Chang
d90a2c4801 Add links to functions
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2020-09-09 13:12:04 -05:00
Winston Chang
f065c21ee6 Merge pull request #2945 from daattali/patch-3
Documentation: say that withProgress returns the value of its express…
2020-09-09 13:11:20 -05:00
Winston Chang
d0324bd497 Merge pull request #3027 from ColinFay/issue-3024 2020-09-09 13:10:31 -05:00
Hadley Wickham
e57fba07db Allow passing server function directory to testServer() (#2965) 2020-09-03 17:01:02 -05:00
Winston Chang
5cb279cf4e Merge pull request #3035 from rstudio/wch-cache-log
Make sure cache logging appends to file
2020-09-02 10:46:45 -05:00
Winston Chang
4f728b0387 Merge pull request #3034 from rstudio/wch-disk-cache-logic 2020-09-02 10:46:33 -05:00
Winston Chang
927ae08a47 Remove unneeded paste0 2020-09-02 10:42:57 -05:00
JooYoung Seo
a28dc47e30 Added es5-shim removal in NEWS.md (#3032) 2020-09-01 21:01:41 -04:00
Winston Chang
b43ee13dd8 Make sure cache logging appends to file 2020-09-01 19:30:42 -05:00
Winston Chang
ad5ad5a675 Update NEWS 2020-09-01 19:20:47 -05:00
Winston Chang
198f7d171e Fix DiskCache pruning logic when max_n and max_size are used. Closes #3033 2020-09-01 19:19:55 -05:00
JooYoung Seo
549425cb81 Merge master branch 2020-08-31 19:12:26 -04:00
JooYoung Seo
6023165268 Merge master branch 2020-08-31 19:02:41 -04:00
Carson Sievert
2a7273c254 Merge pull request #3030 from rstudio/plot-arg-order
Move `alt` parameter after `...`
2020-08-31 17:23:04 -05:00
Winston Chang
8640934410 Move alt parameter after ... 2020-08-31 16:28:43 -05:00
Winston Chang
20bc4e7caa Merge pull request #3006 from jooyoungseo/alttext 2020-08-31 15:49:24 -05:00
Barret Schloerke
9f83058b78 Do not record the reactlog when enabling shinytest (#3025) 2020-08-31 10:39:14 -04:00
colin
ffedf29db4 Other Rds generated by running devtools::document() on the package 2020-08-29 22:27:49 +02:00
colin
00219d342a All update*Input docs now inheritsParams from their standard counterpart
Will close #3024
2020-08-29 22:26:10 +02:00
Winston Chang
753400144d Update diskCache docs and rebuild 2020-08-28 08:58:30 -05:00
Winston Chang
854a732f47 Update NEWS 2020-08-28 08:57:38 -05:00
Winston Chang
03eaf07526 Merge pull request #3016 from aalucaci/issue-2984
Issue 2984
2020-08-28 08:56:16 -05:00
Carson Sievert
d04c12d8cb Merge pull request #3023 from rstudio/carson/bugfix/downloadButton
Change the evaluation rules for the icon promise in downloadButton()
2020-08-27 13:36:44 -05:00
Carson Sievert
afddd3543e Apply suggestions from code review
Co-authored-by: Barret Schloerke <barret@rstudio.com>
2020-08-27 13:35:06 -05:00
Carson
d9459a855d Allow icon arg to reference other downloadButton args 2020-08-27 11:12:09 -05:00
Carson
efbcfca126 Change the evaluation rules for the icon promise in downloadButton(), fixes #3022 2020-08-27 11:07:09 -05:00
Angela Lucaci-Timoce
916675a9bd renderCachedPlot: improved documentation 2020-08-26 21:03:00 +02:00
Colin Fay
7b43617954 downloadButton() icon can now be changed via the icon parameter (#3010) 2020-08-25 15:17:49 -05:00
Winston Chang
09b89bccfd Fix NEWS entry 2020-08-25 13:14:58 -05:00
Winston Chang
1190ee07a9 Remove unused cacheContext class 2020-08-25 13:12:01 -05:00
Barret Schloerke
c4dcf405bb Fix reactiveValuesToList reactlog label (#3017)
Co-authored-by: Barret Schloerke <schloerke@gmail.com>
Co-authored-by: Winston Chang <winston@stdout.org>
2020-08-25 11:49:22 -04:00
JooYoung Seo
c844ea6f07 Added missing alt arg 2020-08-20 10:51:32 -04:00
JooYoung Seo
19704c151a White space formatting to match the lines together
Co-authored-by: Barret Schloerke <barret@rstudio.com>
2020-08-20 10:00:43 -04:00
JooYoung Seo
1281ba18cd Cleaned code 2020-08-19 17:17:40 -04:00
JooYoung Seo
f1f2fae420 Update R/shinyui.R
Used attr instead of attributes()

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2020-08-19 15:43:46 -04:00
JooYoung Seo
5809070b05 Used isTRUE inverse instead of isFALSE 2020-08-19 15:23:36 -04:00
JooYoung Seo
2c492540ce Merged master 2020-08-19 15:04:02 -04:00
JooYoung Seo
61556b505d Used attribute instead of option 2020-08-19 15:02:01 -04:00
Winston Chang
d6a4bc87e8 Fix whitespace 2020-08-19 11:48:35 -05:00
JooYoung Seo
e49f4696e6 Updated NEWS.md 2020-08-19 10:50:23 -04:00
JooYoung Seo
4219f50141 Added a11y semantic landmarks for main and sidebar panels 2020-08-19 10:43:55 -04:00
JooYoung Seo
19da003291 Redocumented for alt param 2020-08-18 17:19:45 -04:00
JooYoung Seo
f0765e3d6a Updated alt param description 2020-08-18 17:05:47 -04:00
JooYoung Seo
8dac345512 Updated NEWS.md 2020-08-18 16:44:43 -04:00
JooYoung Seo
ce101843f0 Made alt param reactive 2020-08-18 15:43:04 -04:00
JooYoung Seo
d56dc3a237 Update R/render-cached-plot.R
Co-authored-by: Barret Schloerke <barret@rstudio.com>
2020-08-18 14:25:16 -04:00
JooYoung Seo
28cffb2e25 Update R/render-plot.R
Co-authored-by: Barret Schloerke <barret@rstudio.com>
2020-08-18 14:24:08 -04:00
JooYoung Seo
d2d169fea3 Update R/render-plot.R
Co-authored-by: Barret Schloerke <barret@rstudio.com>
2020-08-18 14:23:17 -04:00
JooYoung Seo
a5eb1b15d2 Added alt param 2020-08-16 16:11:18 -04:00
JooYoung Seo
6a4c8556a3 Merge branch 'master' into lang-a11y 2020-08-12 15:07:46 -04:00
JooYoung Seo
488f1c8b83 Made sure lang length to be 1 2020-08-12 15:07:23 -04:00
Joe Cheng
1d7a913d29 Add tests 2020-07-17 09:56:33 -07:00
Joe Cheng
f89131205d Warn on 1000+ choices for selectInput/selectizeInput 2020-07-17 09:56:05 -07:00
JooYoung Seo
d616cf045b Resolved NEWS.md 2020-07-15 11:17:52 -04:00
Winston Chang
42af54ca04 Remove context() calls from example app 2020-06-30 13:27:08 -05:00
Dean Attali
9f55cd46d8 Documentation: say that withProgress returns the value of its expression. I didn't know if it does or doesn't and had to test it out, it should be explicit 2020-06-27 13:24:37 -04:00
Dean Attali
5956f6b123 Small documentation addition in withProgress (I did not re-compile roxygen) 2020-06-27 13:18:43 -04:00
JooYoung Seo
8c6a830521 Updated NEWS.md 2020-06-09 15:48:38 -04:00
JooYoung Seo
9142cf19c0 Fixed lang attribute in templates 2020-06-09 14:55:45 -04:00
JooYoung Seo
887b7fb34a Fixed app.r for NULL value of lang argument 2020-06-09 14:29:15 -04:00
JooYoung Seo
1392547783 Redocumentation 2020-06-09 14:20:36 -04:00
JooYoung Seo
735b9b8c7a Updated related scripts, templates, and NEWS.md 2020-06-09 14:19:53 -04:00
JooYoung Seo
cba974ec34 Documentation 2020-06-08 20:10:17 -04:00
JooYoung Seo
421d588a2f Fixed lang attribute in template 2020-06-08 20:09:29 -04:00
JooYoung Seo
8b848277d2 Added lang ShinyOption 2020-06-08 20:08:23 -04:00
JooYoung Seo
8ae19c7243 Added lang attribute to the default template 2020-06-08 16:29:10 -04:00
820 changed files with 65669 additions and 64998 deletions

View File

@@ -12,7 +12,7 @@
^\.travis\.yml$
^staticdocs$
^tools$
^srcjs$
^srcts$
^CONTRIBUTING.md$
^cran-comments.md$
^.*\.o$
@@ -21,3 +21,17 @@
^TODO-promises.md$
^manualtests$
^\.github$
^\.vscode$
^\.madgerc$
^package\.json$
^tsconfig\.json$
^package-lock\.json$
^node_modules$
^coverage$
^.ignore$
^eslint\.config\.mjs$
^_dev$
^.claude$
^README-npm\.md$
^CRAN-SUBMISSION$
^LICENSE\.md$

2
.gitattributes vendored
View File

@@ -1,4 +1,6 @@
/NEWS merge=union
/inst/www/shared/shiny.js -merge -diff
/inst/www/shared/shiny-*.js -merge -diff
/inst/www/shared/shiny*.css -merge -diff
*.min.js -merge -diff
*.js.map -merge -diff

View File

@@ -1,7 +1,7 @@
---
name : Ask a Question
about : The issue tracker is not for questions -- please ask questions at https://community.rstudio.com/c/shiny.
about : The issue tracker is not for questions -- please ask questions at https://forum.posit.co/tags/shiny.
---
The issue tracker is not for questions. If you have a question, please feel free to ask it on our community site, at https://community.rstudio.com/c/shiny.
The issue tracker is not for questions. If you have a question, please feel free to ask it on our community site, at https://forum.posit.co/c/shiny.

13
.github/shiny-workflows/routine.sh vendored Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/bash -e
. ./tools/documentation/checkDocsCurrent.sh
echo "Updating package.json version to match DESCRIPTION Version"
Rscript ./tools/updatePackageJsonVersion.R
if [ -n "$(git status --porcelain package.json)" ]
then
echo "package.json has changed after running ./tools/updatePackageJsonVersion.R. Re-running 'npm run build'"
npm run build
git add ./inst package.json && git commit -m 'Sync package version (GitHub Actions)' || echo "No package version to commit"
else
echo "No package version difference detected; package.json is current."
fi

View File

@@ -1,198 +1,25 @@
name: R-CMD-check
# Workflow derived from https://github.com/rstudio/shiny-workflows
#
# NOTE: This Shiny team GHA workflow is overkill for most R packages.
# For most R packages it is better to use https://github.com/r-lib/actions
on:
push:
branches:
- master
branches: [main, rc-**]
pull_request:
branches:
- master
schedule:
- cron: "0 5 * * 1" # every monday
name: Package checks
jobs:
website:
uses: rstudio/shiny-workflows/.github/workflows/website.yaml@v1
routine:
uses: rstudio/shiny-workflows/.github/workflows/routine.yaml@v1
R-CMD-check:
runs-on: ${{ matrix.config.os }}
name: ${{ matrix.config.os }} (${{ matrix.config.r }})
strategy:
fail-fast: false
matrix:
config:
- {os: macOS-latest, r: 'devel'}
- {os: macOS-latest, r: '4.0'}
- {os: windows-latest, r: '4.0'}
- {os: ubuntu-16.04, r: '4.0', rspm: "https://packagemanager.rstudio.com/cran/__linux__/xenial/latest"}
- {os: ubuntu-16.04, r: '3.6', rspm: "https://packagemanager.rstudio.com/cran/__linux__/xenial/latest"}
- {os: ubuntu-16.04, r: '3.5', rspm: "https://packagemanager.rstudio.com/cran/__linux__/xenial/latest"}
- {os: ubuntu-16.04, r: '3.4', rspm: "https://packagemanager.rstudio.com/cran/__linux__/xenial/latest"}
- {os: ubuntu-16.04, r: '3.3', rspm: "https://packagemanager.rstudio.com/cran/__linux__/xenial/latest"}
env:
_R_CHECK_FORCE_SUGGESTS_: false
R_REMOTES_NO_ERRORS_FROM_WARNINGS: true
RSPM: ${{ matrix.config.rspm }}
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
steps:
# https://github.com/actions/checkout/issues/135
- name: Set git to use LF
if: runner.os == 'Windows'
run: |
git config --system core.autocrlf false
git config --system core.eol lf
- uses: actions/checkout@v2
- uses: r-lib/actions/setup-r@master
with:
r-version: ${{ matrix.config.r }}
- uses: r-lib/actions/setup-pandoc@master
- name: Query dependencies
run: |
install.packages('remotes')
saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2)
shell: Rscript {0}
- name: Cache R packages
if: runner.os != 'Windows'
uses: actions/cache@v1
with:
path: ${{ env.R_LIBS_USER }}
key: ${{ matrix.config.os }}-r-${{ matrix.config.r }}-1-${{ hashFiles('.github/depends.Rds') }}
restore-keys: ${{ matrix.config.os }}-r-${{ matrix.config.r }}-1-
- name: Install system dependencies
if: runner.os == 'Linux'
env:
RHUB_PLATFORM: linux-x86_64-ubuntu-gcc
run: |
Rscript -e "remotes::install_github('r-hub/sysreqs')"
sysreqs=$(Rscript -e "cat(sysreqs::sysreq_commands('DESCRIPTION'))")
sudo -s eval "$sysreqs"
- name: Install dependencies
run: |
remotes::install_deps(dependencies = TRUE)
remotes::install_cran("rcmdcheck")
shell: Rscript {0}
- name: Find PhantomJS path
id: phantomjs
run: |
echo "::set-output name=path::$(Rscript -e 'cat(shinytest:::phantom_paths()[[1]])')"
- name: Cache PhantomJS
uses: actions/cache@v1
with:
path: ${{ steps.phantomjs.outputs.path }}
key: ${{ runner.os }}-phantomjs
restore-keys: ${{ runner.os }}-phantomjs
- name: Install PhantomJS
run: >
Rscript
-e "if (!shinytest::dependenciesInstalled()) shinytest::installDependencies()"
- name: Session info
run: |
options(width = 100)
pkgs <- installed.packages()[, "Package"]
sessioninfo::session_info(pkgs, include_base = TRUE)
shell: Rscript {0}
- name: Check
env:
_R_CHECK_CRAN_INCOMING_: false
run: rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "check")
shell: Rscript {0}
- name: Show testthat output
if: always()
run: find check -name 'testthat.Rout*' -exec cat '{}' \; || true
shell: bash
- name: Upload check results
if: failure()
uses: actions/upload-artifact@v2
with:
name: ${{ runner.os }}-r${{ matrix.config.r }}-results
path: check
documentation:
runs-on: ${{ matrix.config.os }}
name: documentation
strategy:
fail-fast: false
matrix:
config:
- {os: macOS-latest, r: '4.0'}
env:
R_REMOTES_NO_ERRORS_FROM_WARNINGS: true
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: r-lib/actions/setup-r@master
with:
r-version: ${{ matrix.config.r }}
- name: Query dependencies
run: |
install.packages('remotes')
saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2)
shell: Rscript {0}
- name: Cache R packages
uses: actions/cache@v1
with:
path: ${{ env.R_LIBS_USER }}
key: ${{ matrix.config.os }}-r-${{ matrix.config.r }}-2-${{ hashFiles('.github/depends.Rds') }}
restore-keys: ${{ matrix.config.os }}-r-${{ matrix.config.r }}-2-
- name: Remove dependencies file
run: |
rm .github/depends.Rds
- name: Install dependencies
run: |
install.packages(c("remotes"))
remotes::install_deps(dependencies = TRUE)
remotes::install_cran("devtools")
remotes::install_cran("rprojroot")
shell: Rscript {0}
- name: Check documentation
run: |
./tools/documentation/checkDocsCurrent.sh
node_js:
runs-on: macOS-latest
name: node_js
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '12.x'
# https://github.com/actions/cache/blame/ccf96194800dbb7b7094edcd5a7cf3ec3c270f10/examples.md#L185-L200
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: yarn cache
uses: actions/cache@v1
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Check node build
run: |
./tools/checkJSCurrent.sh
uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1
with:
# On R 4.2, Cairo has difficulty installing
# Remove this line when https://github.com/s-u/Cairo/issues/52 is merged
extra-packages: Cairo=?ignore

View File

@@ -1,35 +0,0 @@
on:
issue_comment:
types: [created]
name: Commands
jobs:
document:
if: startsWith(github.event.comment.body, '/document')
name: document
runs-on: macOS-latest
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: r-lib/actions/pr-fetch@master
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: r-lib/actions/setup-r@master
- name: Install dependencies
run: Rscript -e 'install.packages(c("remotes", "roxygen2"))' -e 'remotes::install_deps(dependencies = TRUE)'
- name: Document
run: Rscript -e 'roxygen2::roxygenise()'
- name: commit
run: |
git add man/\* NAMESPACE
git commit -m 'Document'
- uses: r-lib/actions/pr-push@master
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# added so that the workflow doesn't fail.
always_runner:
runs-on: ubuntu-latest
steps:
- name: Always run
run: echo "This job is used to prevent the workflow status from showing as failed when all other jobs are skipped"

14
.gitignore vendored
View File

@@ -9,4 +9,16 @@
shinyapps/
README.html
.*.Rnb.cached
tools/yarn-error.log
/_dev/
.sass_cache_keys
# TypeScript
/node_modules/
.cache
coverage/
madge.svg
# GHA remotes installation
.github/r-depends.rds
.claude/settings.local.json

7
.madgerc Normal file
View File

@@ -0,0 +1,7 @@
{
"detectiveOptions": {
"ts": {
"skipTypeImports": true
}
}
}

6
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

23
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
"search.exclude": {
},
"prettier.prettierPath": "./node_modules/prettier",
"typescript.enablePromptUseWorkspaceTsdk": true,
"[r]": {
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"editor.formatOnSave": false,
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
},
"[json]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
},
}

View File

@@ -1,138 +1,158 @@
Package: shiny
Type: Package
Package: shiny
Title: Web Application Framework for R
Version: 1.5.0.9001
Version: 1.12.1.9000
Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
person("JJ", "Allaire", role = "aut", email = "jj@rstudio.com"),
person("Yihui", "Xie", role = "aut", email = "yihui@rstudio.com"),
person("Jonathan", "McPherson", role = "aut", email = "jonathan@rstudio.com"),
person(family = "RStudio", role = "cph"),
person(family = "jQuery Foundation", role = "cph",
comment = "jQuery library and jQuery UI library"),
person(family = "jQuery contributors", role = c("ctb", "cph"),
comment = "jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt"),
person(family = "jQuery UI contributors", role = c("ctb", "cph"),
comment = "jQuery UI library; authors listed in inst/www/shared/jqueryui/AUTHORS.txt"),
person("Winston", "Chang", , "winston@posit.co", role = "aut",
comment = c(ORCID = "0000-0002-1576-2126")),
person("Joe", "Cheng", , "joe@posit.co", role = "aut"),
person("JJ", "Allaire", , "jj@posit.co", role = "aut"),
person("Carson", "Sievert", , "carson@posit.co", role = c("aut", "cre"),
comment = c(ORCID = "0000-0002-4958-2844")),
person("Barret", "Schloerke", , "barret@posit.co", role = "aut",
comment = c(ORCID = "0000-0001-9986-114X")),
person("Garrick", "Aden-Buie", , "garrick@adenbuie.com", role = "aut",
comment = c(ORCID = "0000-0002-7111-0077")),
person("Yihui", "Xie", , "yihui@posit.co", role = "aut"),
person("Jeff", "Allen", role = "aut"),
person("Jonathan", "McPherson", , "jonathan@posit.co", role = "aut"),
person("Alan", "Dipert", role = "aut"),
person("Barbara", "Borges", role = "aut"),
person("Posit Software, PBC", role = c("cph", "fnd"),
comment = c(ROR = "03wc8by49")),
person(, "jQuery Foundation", role = "cph",
comment = "jQuery library and jQuery UI library"),
person(, "jQuery contributors", role = c("ctb", "cph"),
comment = "jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt"),
person(, "jQuery UI contributors", role = c("ctb", "cph"),
comment = "jQuery UI library; authors listed in inst/www/shared/jqueryui/AUTHORS.txt"),
person("Mark", "Otto", role = "ctb",
comment = "Bootstrap library"),
comment = "Bootstrap library"),
person("Jacob", "Thornton", role = "ctb",
comment = "Bootstrap library"),
person(family = "Bootstrap contributors", role = "ctb",
comment = "Bootstrap library"),
person(family = "Twitter, Inc", role = "cph",
comment = "Bootstrap library"),
comment = "Bootstrap library"),
person(, "Bootstrap contributors", role = "ctb",
comment = "Bootstrap library"),
person(, "Twitter, Inc", role = "cph",
comment = "Bootstrap library"),
person("Prem Nawaz", "Khan", role = "ctb",
comment = "Bootstrap accessibility plugin"),
comment = "Bootstrap accessibility plugin"),
person("Victor", "Tsaran", role = "ctb",
comment = "Bootstrap accessibility plugin"),
comment = "Bootstrap accessibility plugin"),
person("Dennis", "Lembree", role = "ctb",
comment = "Bootstrap accessibility plugin"),
comment = "Bootstrap accessibility plugin"),
person("Srinivasu", "Chakravarthula", role = "ctb",
comment = "Bootstrap accessibility plugin"),
comment = "Bootstrap accessibility plugin"),
person("Cathy", "O'Connor", role = "ctb",
comment = "Bootstrap accessibility plugin"),
person(family = "PayPal, Inc", role = "cph",
comment = "Bootstrap accessibility plugin"),
comment = "Bootstrap accessibility plugin"),
person(, "PayPal, Inc", role = "cph",
comment = "Bootstrap accessibility plugin"),
person("Stefan", "Petre", role = c("ctb", "cph"),
comment = "Bootstrap-datepicker library"),
comment = "Bootstrap-datepicker library"),
person("Andrew", "Rowls", role = c("ctb", "cph"),
comment = "Bootstrap-datepicker library"),
person("Dave", "Gandy", role = c("ctb", "cph"),
comment = "Font-Awesome font"),
comment = "Bootstrap-datepicker library"),
person("Brian", "Reavis", role = c("ctb", "cph"),
comment = "selectize.js library"),
comment = "selectize.js library"),
person("Salmen", "Bejaoui", role = c("ctb", "cph"),
comment = "selectize-plugin-a11y library"),
comment = "selectize-plugin-a11y library"),
person("Denis", "Ineshin", role = c("ctb", "cph"),
comment = "ion.rangeSlider library"),
comment = "ion.rangeSlider library"),
person("Sami", "Samhuri", role = c("ctb", "cph"),
comment = "Javascript strftime library"),
person(family = "SpryMedia Limited", role = c("ctb", "cph"),
comment = "DataTables library"),
person("John", "Fraser", role = c("ctb", "cph"),
comment = "showdown.js library"),
person("John", "Gruber", role = c("ctb", "cph"),
comment = "showdown.js library"),
comment = "Javascript strftime library"),
person(, "SpryMedia Limited", role = c("ctb", "cph"),
comment = "DataTables library"),
person("Ivan", "Sagalaev", role = c("ctb", "cph"),
comment = "highlight.js library"),
person(family = "R Core Team", role = c("ctb", "cph"),
comment = "tar implementation from R")
)
comment = "highlight.js library"),
person("R Core Team", role = c("ctb", "cph"),
comment = "tar implementation from R")
)
Description: Makes it incredibly easy to build interactive web
applications with R. Automatic "reactive" binding between inputs and
outputs and extensive prebuilt widgets make it possible to build
beautiful, responsive, and powerful applications with minimal effort.
License: GPL-3 | file LICENSE
Depends:
R (>= 3.0.2),
methods
Imports:
utils,
grDevices,
httpuv (>= 1.5.2),
mime (>= 0.3),
jsonlite (>= 0.9.16),
xtable,
digest,
htmltools (>= 0.4.0.9003),
R6 (>= 2.0),
sourcetools,
later (>= 1.0.0),
promises (>= 1.1.0),
tools,
crayon,
rlang (>= 0.4.0),
fastmap (>= 1.0.0),
withr,
commonmark (>= 1.7),
glue (>= 1.3.2)
Suggests:
datasets,
Cairo (>= 1.5-5),
testthat (>= 2.1.1),
knitr (>= 1.6),
markdown,
rmarkdown,
ggplot2,
reactlog (>= 1.0.0),
magrittr,
shinytest,
yaml,
future,
dygraphs,
ragg,
showtext
URL: http://shiny.rstudio.com
License: MIT + file LICENSE
URL: https://shiny.posit.co/, https://github.com/rstudio/shiny
BugReports: https://github.com/rstudio/shiny/issues
Depends:
methods,
R (>= 3.0.2)
Imports:
bslib (>= 0.6.0),
cachem (>= 1.1.0),
cli,
commonmark (>= 2.0.0),
fastmap (>= 1.1.1),
fontawesome (>= 0.4.0),
glue (>= 1.3.2),
grDevices,
htmltools (>= 0.5.4),
httpuv (>= 1.5.2),
jsonlite (>= 0.9.16),
later (>= 1.0.0),
lifecycle (>= 0.2.0),
mime (>= 0.3),
otel,
promises (>= 1.5.0),
R6 (>= 2.0),
rlang (>= 0.4.10),
sourcetools,
tools,
utils,
withr,
xtable
Suggests:
Cairo (>= 1.5-5),
coro (>= 1.1.0),
datasets,
DT,
dygraphs,
future,
ggplot2,
knitr (>= 1.6),
magrittr,
markdown,
mirai,
otelsdk (>= 0.2.0),
ragg,
reactlog (>= 1.0.0),
rmarkdown,
sass,
showtext,
testthat (>= 3.2.1),
watcher,
yaml
Config/Needs/check: shinytest2
Config/testthat/edition: 3
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.3
Collate:
'app.R'
'globals.R'
'app-state.R'
'app_template.R'
'bind-cache.R'
'bind-event.R'
'bookmark-state-local.R'
'stack.R'
'bookmark-state.R'
'bootstrap-deprecated.R'
'bootstrap-layout.R'
'globals.R'
'conditions.R'
'map.R'
'utils.R'
'bootstrap.R'
'cache-context.R'
'cache-disk.R'
'cache-memory.R'
'busy-indicators-spinners.R'
'busy-indicators.R'
'cache-utils.R'
'deprecated.R'
'devmode.R'
'diagnose.R'
'extended-task.R'
'fileupload.R'
'font-awesome.R'
'graph.R'
'reactives.R'
'reactive-domains.R'
'history.R'
'hooks.R'
'html-deps.R'
'htmltools.R'
'image-interact-opts.R'
'image-interact.R'
'imageutils.R'
@@ -154,6 +174,7 @@ Collate:
'insert-tab.R'
'insert-ui.R'
'jqueryui.R'
'knitr.R'
'middleware-shiny.R'
'middleware.R'
'timer.R'
@@ -162,6 +183,15 @@ Collate:
'modal.R'
'modules.R'
'notifications.R'
'otel-attr-srcref.R'
'otel-collect.R'
'otel-enable.R'
'otel-error.R'
'otel-label.R'
'otel-reactive-update.R'
'otel-session.R'
'otel-shiny.R'
'otel-with.R'
'priorityqueue.R'
'progress.R'
'react.R'
@@ -170,19 +200,30 @@ Collate:
'render-plot.R'
'render-table.R'
'run-url.R'
'runapp.R'
'serializers.R'
'server-input-handlers.R'
'server-resource-paths.R'
'server.R'
'shiny-options.R'
'shiny-package.R'
'shinyapp.R'
'shinyui.R'
'shinywrappers.R'
'showcase.R'
'snapshot.R'
'staticimports.R'
'tar.R'
'test-export.R'
'test-server.R'
'test.R'
'update-input.R'
RoxygenNote: 7.1.1
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
'utils-lang.R'
'utils-tags.R'
'version_bs_date_picker.R'
'version_ion_range_slider.R'
'version_jquery.R'
'version_jqueryui.R'
'version_selectize.R'
'version_strftime.R'
'viewer.R'

1460
LICENSE

File diff suppressed because it is too large Load Diff

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
# MIT License
Copyright (c) 2025 shiny authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1011
LICENSE.note Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,14 +19,32 @@ S3method("[[",shinyoutput)
S3method("[[<-",reactivevalues)
S3method("[[<-",shinyoutput)
S3method("names<-",reactivevalues)
S3method(as.list,Map)
S3method(as.list,reactivevalues)
S3method(as.shiny.appobj,character)
S3method(as.shiny.appobj,list)
S3method(as.shiny.appobj,shiny.appobj)
S3method(as.tags,shiny.appobj)
S3method(as.tags,shiny.render.function)
S3method(bindCache,"function")
S3method(bindCache,Observer)
S3method(bindCache,default)
S3method(bindCache,reactive.cache)
S3method(bindCache,reactive.event)
S3method(bindCache,reactiveExpr)
S3method(bindCache,shiny.render.function)
S3method(bindCache,shiny.render.function.cache)
S3method(bindCache,shiny.render.function.event)
S3method(bindCache,shiny.renderPlot)
S3method(bindEvent,Observer)
S3method(bindEvent,Observer.event)
S3method(bindEvent,default)
S3method(bindEvent,reactive.event)
S3method(bindEvent,reactiveExpr)
S3method(bindEvent,shiny.render.function)
S3method(format,reactiveExpr)
S3method(format,reactiveVal)
S3method(length,Map)
S3method(names,reactivevalues)
S3method(print,reactive)
S3method(print,reactivevalues)
@@ -37,6 +55,7 @@ S3method(str,reactivevalues)
export("conditionStackTrace<-")
export(..stacktraceoff..)
export(..stacktraceon..)
export(ExtendedTask)
export(HTML)
export(MockShinySession)
export(NS)
@@ -50,6 +69,8 @@ export(animationOptions)
export(appendTab)
export(as.shiny.appobj)
export(basicPage)
export(bindCache)
export(bindEvent)
export(bookmarkButton)
export(bootstrapLib)
export(bootstrapPage)
@@ -57,6 +78,7 @@ export(br)
export(browserViewer)
export(brushOpts)
export(brushedPoints)
export(busyIndicatorOptions)
export(callModule)
export(captureStackTraces)
export(checkboxGroupInput)
@@ -73,6 +95,7 @@ export(dateInput)
export(dateRangeInput)
export(dblclickOpts)
export(debounce)
export(devmode)
export(dialogViewer)
export(diskCache)
export(div)
@@ -84,7 +107,6 @@ export(enableBookmarking)
export(eventReactive)
export(exportTestValues)
export(exprToFunction)
export(extractStackTrace)
export(fileInput)
export(fillCol)
export(fillPage)
@@ -95,14 +117,15 @@ export(fixedRow)
export(flowLayout)
export(fluidPage)
export(fluidRow)
export(formatStackTrace)
export(freezeReactiveVal)
export(freezeReactiveValue)
export(getCurrentOutputInfo)
export(getCurrentTheme)
export(getDefaultReactiveDomain)
export(getQueryString)
export(getShinyOption)
export(getUrlHash)
export(get_devmode_option)
export(h1)
export(h2)
export(h3)
@@ -120,6 +143,7 @@ export(httpResponse)
export(icon)
export(imageOutput)
export(img)
export(in_devmode)
export(incProgress)
export(includeCSS)
export(includeHTML)
@@ -141,6 +165,7 @@ export(isTruthy)
export(isolate)
export(key_missing)
export(loadSupport)
export(localOtelCollect)
export(mainPanel)
export(makeReactiveBinding)
export(markRenderFunction)
@@ -168,6 +193,7 @@ export(onRestore)
export(onRestored)
export(onSessionEnded)
export(onStop)
export(onUnhandledError)
export(outputOptions)
export(p)
export(pageWithSidebar)
@@ -180,23 +206,23 @@ export(pre)
export(prependTab)
export(printError)
export(printStackTrace)
export(quoToFunction)
export(radioButtons)
export(reactive)
export(reactiveConsole)
export(reactiveFileReader)
export(reactivePlot)
export(reactivePoll)
export(reactivePrint)
export(reactiveTable)
export(reactiveText)
export(reactiveTimer)
export(reactiveUI)
export(reactiveVal)
export(reactiveValues)
export(reactiveValuesToList)
export(reactlog)
export(reactlogAddMark)
export(reactlogReset)
export(reactlogShow)
export(registerInputHandler)
export(registerThemeDependency)
export(register_devmode_option)
export(removeInputHandler)
export(removeModal)
export(removeNotification)
@@ -239,7 +265,6 @@ export(shinyUI)
export(showBookmarkUrlModal)
export(showModal)
export(showNotification)
export(showReactLog)
export(showTab)
export(sidebarLayout)
export(sidebarPanel)
@@ -295,6 +320,7 @@ export(updateTextInput)
export(updateVarSelectInput)
export(updateVarSelectizeInput)
export(urlModal)
export(useBusyIndicators)
export(validate)
export(validateCssUnit)
export(varSelectInput)
@@ -304,11 +330,12 @@ export(verticalLayout)
export(wellPanel)
export(withLogErrors)
export(withMathJax)
export(withOtelCollect)
export(withProgress)
export(withReactiveDomain)
export(withTags)
export(with_devmode)
import(R6)
import(digest)
import(htmltools)
import(httpuv)
import(methods)
@@ -357,5 +384,49 @@ importFrom(htmltools,tagSetChildren)
importFrom(htmltools,tags)
importFrom(htmltools,validateCssUnit)
importFrom(htmltools,withTags)
importFrom(lifecycle,deprecated)
importFrom(lifecycle,is_present)
importFrom(promises,"%...!%")
importFrom(promises,"%...>%")
importFrom(promises,as.promise)
importFrom(promises,hybrid_then)
importFrom(promises,is.promise)
importFrom(promises,is.promising)
importFrom(promises,new_promise_domain)
importFrom(promises,promise_reject)
importFrom(promises,promise_resolve)
importFrom(promises,with_promise_domain)
importFrom(rlang,"%||%")
importFrom(rlang,"fn_body<-")
importFrom(rlang,"fn_fmls<-")
importFrom(rlang,as_function)
importFrom(rlang,as_quosure)
importFrom(rlang,check_dots_empty)
importFrom(rlang,check_dots_unnamed)
importFrom(rlang,enexpr)
importFrom(rlang,enquo)
importFrom(rlang,enquo0)
importFrom(rlang,enquos)
importFrom(rlang,enquos0)
importFrom(rlang,eval_tidy)
importFrom(rlang,expr)
importFrom(rlang,fn_body)
importFrom(rlang,get_env)
importFrom(rlang,get_expr)
importFrom(rlang,inject)
importFrom(rlang,is_false)
importFrom(rlang,is_missing)
importFrom(rlang,is_na)
importFrom(rlang,is_quosure)
importFrom(rlang,list2)
importFrom(rlang,maybe_missing)
importFrom(rlang,missing_arg)
importFrom(rlang,new_function)
importFrom(rlang,new_quosure)
importFrom(rlang,pairlist2)
importFrom(rlang,quo)
importFrom(rlang,quo_get_expr)
importFrom(rlang,quo_is_missing)
importFrom(rlang,quo_set_env)
importFrom(rlang,quo_set_expr)
importFrom(rlang,zap_srcref)

796
NEWS.md

File diff suppressed because it is too large Load Diff

26
R/app-state.R Normal file
View File

@@ -0,0 +1,26 @@
#' @include globals.R
NULL
# The current app state is a place to read and hang state for the
# currently-running application. This is useful for setting options that will
# last as long as the application is running.
.globals$appState <- NULL
initCurrentAppState <- function(appobj) {
if (!is.null(.globals$appState)) {
stop("Can't initialize current app state when another is currently active.")
}
.globals$appState <- new.env(parent = emptyenv())
.globals$appState$app <- appobj
# Copy over global options
.globals$appState$options <- .globals$options
}
getCurrentAppState <- function() {
.globals$appState
}
clearCurrentAppState <- function() {
.globals$appState <- NULL
}

View File

@@ -10,8 +10,7 @@
#' 2: app.R : Main application file
#' 3: R/example.R : Helper file with R code
#' 4: R/example-module.R : Example module
#' 5: tests/shinytest/ : Tests using the shinytest package
#' 6: tests/testthat/ : Tests using the testthat package
#' 5: tests/testthat/ : Tests using the testthat and shinytest2 package
#' ```
#'
#' If option 1 is selected, the full example application including the
@@ -24,13 +23,12 @@
#' | |- example-module.R
#' | `- example.R
#' `- tests
#' |- shinytest.R
#' |- shinytest
#' | `- mytest.R
#' |- testthat.R
#' `- testthat
#' |- setup-shinytest2.R
#' |- test-examplemodule.R
#' |- test-server.R
#' |- test-shinytest2.R
#' `- test-sort.R
#' ```
#'
@@ -45,20 +43,21 @@
#' * `tests/` contains various tests for the application. You may
#' choose to use or remove any of them. They can be executed by the
#' [runTests()] function.
#' * `tests/shinytest.R` is a test runner for test files in the
#' `tests/shinytest/` directory.
#' * `tests/shinytest/mytest.R` is a test that uses the
#' [shinytest](https://rstudio.github.io/shinytest/) package to do
#' snapshot-based testing.
#' * `tests/testthat.R` is a test runner for test files in the
#' `tests/testthat/` directory using the [testthat](https://testthat.r-lib.org/) package.
#' `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
#' [shinytest2](https://rstudio.github.io/shinytest2/) package to do
#' snapshot-based testing.
#' * `tests/testthat/test-sort.R` is a test for a supporting function in the `R/` directory.
#'
#' @param path Path to create new shiny application template.
#' @param examples Either one of "default", "ask", "all", or any combination of
#' "app", "rdir", "module", "shinytest", and "testthat". In an
#' "app", "rdir", "module", and "tests". In an
#' interactive session, "default" falls back to "ask"; in a non-interactive
#' session, "default" falls back to "all". With "ask", this function will
#' prompt the user to select which template items will be added to the new app
@@ -79,15 +78,19 @@ shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
# =======================================================
choices <- c(
app = "app.R : Main application file",
rdir = "R/example.R : Helper file with R code",
module = "R/example-module.R : Example module",
shinytest = "tests/shinytest/ : Tests using the shinytest package",
testthat = "tests/testthat/ : Tests using the testthat package"
app = "app.R : Main application file",
rdir = "R/example.R : Helper file with R code",
module = "R/example-module.R : Example module",
tests = "tests/testthat/ : Tests using {testthat} and {shinytest2}"
)
# Support legacy value
examples[examples == "shinytest"] <- "tests"
examples[examples == "testthat"] <- "tests"
examples <- unique(examples)
if (identical(examples, "default")) {
if (interactive()) {
if (rlang::is_interactive()) {
examples <- "ask"
} else {
examples <- "all"
@@ -124,18 +127,8 @@ shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
return(invisible())
}
if ("shinytest" %in% examples) {
if (!is_available("shinytest", "1.4.0"))
{
message(
"The tests/shinytest directory needs shinytest 1.4.0 or later to work properly."
)
if (is_available("shinytest")) {
message("You currently have shinytest ",
utils::packageVersion("shinytest"), " installed.")
}
}
if ("tests" %in% examples) {
rlang::check_installed("shinytest2", "for {testthat} tests to work as expected", version = "0.2.0")
}
# =======================================================
@@ -152,7 +145,7 @@ shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
# Helper to resolve paths relative to our template
template_path <- function(...) {
system.file("app_template", ..., package = "shiny")
system_file("app_template", ..., package = "shiny")
}
# Resolve path relative to destination
@@ -208,16 +201,13 @@ shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
}
# Copy the files for a tests/ subdirectory
copy_test_dir <- function(name) {
copy_test_dir <- function() {
files <- dir(template_path("tests"), recursive = TRUE)
# Note: This is not the same as using dir(pattern = "^shinytest"), since
# that will not match files inside of shinytest/.
files <- files[grepl(paste0("^", name), files)]
# Filter out files that are not module files in the R directory.
if (! "rdir" %in% examples) {
# find all files in the testthat folder that are not module or server files
is_r_folder_file <- (!grepl("module|server", basename(files))) & (dirname(files) == "testthat")
is_r_folder_file <- !grepl("module|server|shinytest2|testthat", basename(files))
files <- files[!is_r_folder_file]
}
@@ -282,12 +272,10 @@ shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
copy_file(file.path("R", module_files))
}
# tests/ dir
if ("shinytest" %in% examples) {
copy_test_dir("shinytest")
}
if ("testthat" %in% examples) {
copy_test_dir("testthat")
# tests/testthat dir
if ("tests" %in% examples) {
copy_test_dir()
}
invisible()
}

793
R/bind-cache.R Normal file
View File

@@ -0,0 +1,793 @@
utils::globalVariables(".GenericCallEnv", add = TRUE)
#' Add caching with reactivity to an object
#'
#' @description
#'
#' `bindCache()` adds caching [reactive()] expressions and `render*` functions
#' (like [renderText()], [renderTable()], ...).
#'
#' Ordinary [reactive()] expressions automatically cache their _most recent_
#' value, which helps to avoid redundant computation in downstream reactives.
#' `bindCache()` will cache all previous values (as long as they fit in the
#' cache) and they can be shared across user sessions. This allows
#' `bindCache()` to dramatically improve performance when used correctly.
#' @details
#'
#' `bindCache()` requires one or more expressions that are used to generate a
#' **cache key**, which is used to determine if a computation has occurred
#' before and hence can be retrieved from the cache. If you're familiar with the
#' concept of memoizing pure functions (e.g., the \pkg{memoise} package), you
#' can think of the cache key as the input(s) to a pure function. As such, one
#' should take care to make sure the use of `bindCache()` is _pure_ in the same
#' sense, namely:
#'
#' 1. For a given key, the return value is always the same.
#' 2. Evaluation has no side-effects.
#'
#' In the example here, the `bindCache()` key consists of `input$x` and
#' `input$y` combined, and the value is `input$x * input$y`. In this simple
#' example, for any given key, there is only one possible returned value.
#'
#' ```
#' r <- reactive({ input$x * input$y }) %>%
#' bindCache(input$x, input$y)
#' ```
#'
#' The largest performance improvements occur when the cache key is fast to
#' compute and the reactive expression is slow to compute. To see if the value
#' should be computed, a cached reactive evaluates the key, and then serializes
#' and hashes the result. If the resulting hashed key is in the cache, then the
#' cached reactive simply retrieves the previously calculated value and returns
#' it; if not, then the value is computed and the result is stored in the cache
#' before being returned.
#'
#' To compute the cache key, `bindCache()` hashes the contents of `...`, so it's
#' best to avoid including large objects in a cache key since that can result in
#' slow hashing. It's also best to avoid reference objects like environments and
#' R6 objects, since the serialization of these objects may not capture relevant
#' changes.
#'
#' If you want to use a large object as part of a cache key, it may make sense
#' to do some sort of reduction on the data that still captures information
#' about whether a value can be retrieved from the cache. For example, if you
#' have a large data set with timestamps, it might make sense to extract the
#' most recent timestamp and return that. Then, instead of hashing the entire
#' data object, the cached reactive only needs to hash the timestamp.
#'
#' ```
#' r <- reactive({ compute(bigdata()) } %>%
#' bindCache({ extract_most_recent_time(bigdata()) })
#' ```
#'
#' For computations that are very slow, it often makes sense to pair
#' [bindCache()] with [bindEvent()] so that no computation is performed until
#' the user explicitly requests it (for more, see the Details section of
#' [bindEvent()]).
#' @section Cache keys and reactivity:
#'
#' Because the **value** expression (from the original [reactive()]) is
#' cached, it is not necessarily re-executed when someone retrieves a value,
#' and therefore it can't be used to decide what objects to take reactive
#' dependencies on. Instead, the **key** is used to figure out which objects
#' to take reactive dependencies on. In short, the key expression is reactive,
#' and value expression is no longer reactive.
#'
#' Here's an example of what not to do: if the key is `input$x` and the value
#' expression is from `reactive({input$x + input$y})`, then the resulting
#' cached reactive will only take a reactive dependency on `input$x` -- it
#' won't recompute `{input$x + input$y}` when just `input$y` changes.
#' Moreover, the cache won't use `input$y` as part of the key, and so it could
#' return incorrect values in the future when it retrieves values from the
#' cache. (See the examples below for an example of this.)
#'
#' A better cache key would be something like `input$x, input$y`. This does
#' two things: it ensures that a reactive dependency is taken on both
#' `input$x` and `input$y`, and it also makes sure that both values are
#' represented in the cache key.
#'
#' In general, `key` should use the same reactive inputs as `value`, but the
#' computation should be simpler. If there are other (non-reactive) values
#' that are consumed, such as external data sources, they should be used in
#' the `key` as well. Note that if the `key` is large, it can make sense to do
#' some sort of reduction on it so that the serialization and hashing of the
#' cache key is not too expensive.
#'
#' Remember that the key is _reactive_, so it is not re-executed every single
#' time that someone accesses the cached reactive. It is only re-executed if
#' it has been invalidated by one of the reactives it depends on. For
#' example, suppose we have this cached reactive:
#'
#' ```
#' r <- reactive({ input$x * input$y }) %>%
#' bindCache(input$x, input$y)
#' ```
#'
#' In this case, the key expression is essentially `reactive(list(input$x,
#' input$y))` (there's a bit more to it, but that's a good enough
#' approximation). The first time `r()` is called, it executes the key, then
#' fails to find it in the cache, so it executes the value expression, `{
#' input$x + input$y }`. If `r()` is called again, then it does not need to
#' re-execute the key expression, because it has not been invalidated via a
#' change to `input$x` or `input$y`; it simply returns the previous value.
#' However, if `input$x` or `input$y` changes, then the reactive expression will
#' be invalidated, and the next time that someone calls `r()`, the key
#' expression will need to be re-executed.
#'
#' Note that if the cached reactive is passed to [bindEvent()], then the key
#' expression will no longer be reactive; instead, the event expression will be
#' reactive.
#'
#'
#' @section Cache scope:
#'
#' By default, when `bindCache()` is used, it is scoped to the running
#' application. That means that it shares a cache with all user sessions
#' connected to the application (within the R process). This is done with the
#' `cache` parameter's default value, `"app"`.
#'
#' With an app-level cache scope, one user can benefit from the work done for
#' another user's session. In most cases, this is the best way to get
#' performance improvements from caching. However, in some cases, this could
#' leak information between sessions. For example, if the cache key does not
#' fully encompass the inputs used by the value, then data could leak between
#' the sessions. Or if a user sees that a cached reactive returns its value
#' very quickly, they may be able to infer that someone else has already used
#' it with the same values.
#'
#' It is also possible to scope the cache to the session, with
#' `cache="session"`. This removes the risk of information leaking between
#' sessions, but then one session cannot benefit from computations performed in
#' another session.
#'
#' It is possible to pass in caching objects directly to
#' `bindCache()`. This can be useful if, for example, you want to use a
#' particular type of cache with specific cached reactives, or if you want to
#' use a [cachem::cache_disk()] that is shared across multiple processes and
#' persists beyond the current R session.
#'
#' To use different settings for an application-scoped cache, you can call
#' [shinyOptions()] at the top of your app.R, server.R, or
#' global.R. For example, this will create a cache with 500 MB of space
#' instead of the default 200 MB:
#'
#' ```
#' shinyOptions(cache = cachem::cache_mem(max_size = 500e6))
#' ```
#'
#' To use different settings for a session-scoped cache, you can set
#' `session$cache` at the top of your server function. By default, it will
#' create a 200 MB memory cache for each session, but you can replace it with
#' something different. To use the session-scoped cache, you must also call
#' `bindCache()` with `cache="session"`. This will create a 100 MB cache for
#' the session:
#'
#' ```
#' function(input, output, session) {
#' session$cache <- cachem::cache_mem(max_size = 100e6)
#' ...
#' }
#' ```
#'
#' If you want to use a cache that is shared across multiple R processes, you
#' can use a [cachem::cache_disk()]. You can create a application-level shared
#' cache by putting this at the top of your app.R, server.R, or global.R:
#'
#' ```
#' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache")))
#' ```
#'
#' This will create a subdirectory in your system temp directory named
#' `myapp-cache` (replace `myapp-cache` with a unique name of
#' your choosing). On most platforms, this directory will be removed when
#' your system reboots. This cache will persist across multiple starts and
#' stops of the R process, as long as you do not reboot.
#'
#' To have the cache persist even across multiple reboots, you can create the
#' cache in a location outside of the temp directory. For example, it could
#' be a subdirectory of the application:
#'
#' ```
#' shinyOptions(cache = cachem::cache_disk("./myapp-cache"))
#' ```
#'
#' In this case, resetting the cache will have to be done manually, by deleting
#' the directory.
#'
#' You can also scope a cache to just one item, or selected items. To do that,
#' create a [cachem::cache_mem()] or [cachem::cache_disk()], and pass it
#' as the `cache` argument of `bindCache()`.
#'
#'
#' @section Computing cache keys:
#'
#' The actual cache key that is used internally takes value from evaluating
#' the key expression(s) (from the `...` arguments) and combines it with the
#' (unevaluated) value expression.
#'
#' This means that if there are two cached reactives which have the same
#' result from evaluating the key, but different value expressions, then they
#' will not need to worry about collisions.
#'
#' However, if two cached reactives have identical key and value expressions
#' expressions, they will share the cached values. This is useful when using
#' `cache="app"`: there may be multiple user sessions which create separate
#' cached reactive objects (because they are created from the same code in the
#' server function, but the server function is executed once for each user
#' session), and those cached reactive objects across sessions can share
#' values in the cache.
#'
#' @section Async with cached reactives:
#'
#' With a cached reactive expression, the key and/or value expression can be
#' _asynchronous_. In other words, they can be promises --- not regular R
#' promises, but rather objects provided by the
#' \href{https://rstudio.github.io/promises/}{\pkg{promises}} package, which
#' are similar to promises in JavaScript. (See [promises::promise()] for more
#' information.) You can also use [mirai::mirai()] or [future::future()]
#' objects to run code in a separate process or even on a remote machine.
#'
#' If the value returns a promise, then anything that consumes the cached
#' reactive must expect it to return a promise.
#'
#' Similarly, if the key is a promise (in other words, if it is asynchronous),
#' then the entire cached reactive must be asynchronous, since the key must be
#' computed asynchronously before it knows whether to compute the value or the
#' value is retrieved from the cache. Anything that consumes the cached
#' reactive must therefore expect it to return a promise.
#'
#'
#' @section Developing render functions for caching:
#'
#' If you've implemented your own `render*()` function, it may just work with
#' `bindCache()`, but it is possible that you will need to make some
#' modifications. These modifications involve helping `bindCache()` avoid
#' cache collisions, dealing with internal state that may be set by the,
#' `render` function, and modifying the data as it goes in and comes out of
#' the cache.
#'
#' You may need to provide a `cacheHint` to [createRenderFunction()] (or
#' `htmlwidgets::shinyRenderWidget()`, if you've authored an htmlwidget) in
#' order for `bindCache()` to correctly compute a cache key.
#'
#' The potential problem is a cache collision. Consider the following:
#'
#' ```
#' output$x1 <- renderText({ input$x }) %>% bindCache(input$x)
#' output$x2 <- renderText({ input$x * 2 }) %>% bindCache(input$x)
#' ```
#'
#' Both `output$x1` and `output$x2` use `input$x` as part of their cache key,
#' but if it were the only thing used in the cache key, then the two outputs
#' would have a cache collision, and they would have the same output. To avoid
#' this, a _cache hint_ is automatically added when [renderText()] calls
#' [createRenderFunction()]. The cache hint is used as part of the actual
#' cache key, in addition to the one passed to `bindCache()` by the user. The
#' cache hint can be viewed by calling the internal Shiny function
#' `extractCacheHint()`:
#'
#' ```
#' r <- renderText({ input$x })
#' shiny:::extractCacheHint(r)
#' ```
#'
#' This returns a nested list containing an item, `$origUserFunc$body`, which
#' in this case is the expression which was passed to `renderText()`:
#' `{ input$x }`. This (quoted) expression is mixed into the actual cache
#' key, and it is how `output$x1` does not have collisions with `output$x2`.
#'
#' For most developers of render functions, nothing extra needs to be done;
#' the automatic inference of the cache hint is sufficient. Again, you can
#' check it by calling `shiny:::extractCacheHint()`, and by testing the
#' render function for cache collisions in a real application.
#'
#' In some cases, however, the automatic cache hint inference is not
#' sufficient, and it is necessary to provide a cache hint. This is true
#' for `renderPrint()`. Unlike `renderText()`, it wraps the user-provided
#' expression in another function, before passing it to [createRenderFunction()]
#' (instead of [createRenderFunction()]). Because the user code is wrapped in
#' another function, `createRenderFunction()` is not able to automatically
#' extract the user-provided code and use it in the cache key. Instead,
#' `renderPrint` calls `createRenderFunction()`, it explicitly passes along a
#' `cacheHint`, which includes a label and the original user expression.
#'
#' In general, if you need to provide a `cacheHint`, it is best practice to
#' provide a `label` id, the user's `expr`, as well as any other arguments
#' that may influence the final value.
#'
#' For \pkg{htmlwidgets}, it will try to automatically infer a cache hint;
#' again, you can inspect the cache hint with `shiny:::extractCacheHint()` and
#' also test it in an application. If you do need to explicitly provide a
#' cache hint, pass it to `shinyRenderWidget`. For example:
#'
#' ```
#' renderMyWidget <- function(expr) {
#' q <- rlang::enquo0(expr)
#'
#' htmlwidgets::shinyRenderWidget(
#' q,
#' myWidgetOutput,
#' quoted = TRUE,
#' cacheHint = list(label = "myWidget", userQuo = q)
#' )
#' }
#' ```
#'
#' If your `render` function sets any internal state, you may find it useful
#' in your call to [createRenderFunction()] to use
#' the `cacheWriteHook` and/or `cacheReadHook` parameters. These hooks are
#' functions that run just before the object is stored in the cache, and just
#' after the object is retrieved from the cache. They can modify the data
#' that is stored and retrieved; this can be useful if extra information needs
#' to be stored in the cache. They can also be used to modify the state of the
#' application; for example, it can call [createWebDependency()] to make
#' JS/CSS resources available if the cached object is loaded in a different R
#' process. (See the source of `htmlwidgets::shinyRenderWidget` for an example
#' of this.)
#'
#' @section Uncacheable objects:
#'
#' Some render functions cannot be cached, typically because they have side
#' effects or modify some external state, and they must re-execute each time
#' in order to work properly.
#'
#' For developers of such code, they should call [createRenderFunction()] (or
#' [markRenderFunction()]) with `cacheHint = FALSE`.
#'
#'
#' @section Caching with `renderPlot()`:
#'
#' When `bindCache()` is used with `renderPlot()`, the `height` and `width`
#' passed to the original `renderPlot()` are ignored. They are superseded by
#' `sizePolicy` argument passed to `bindCache. The default is:
#'
#' ```
#' sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2)
#' ```
#'
#' `sizePolicy` must be a function that takes a two-element numeric vector as
#' input, representing the width and height of the `<img>` element in the
#' browser window, and it must return a two-element numeric vector, representing
#' the pixel dimensions of the plot to generate. The purpose is to round the
#' actual pixel dimensions from the browser to some other dimensions, so that
#' this will not generate and cache images of every possible pixel dimension.
#' See [sizeGrowthRatio()] for more information on the default sizing policy.
#'
#' @param x The object to add caching to.
#' @param ... One or more expressions to use in the caching key.
#' @param cache The scope of the cache, or a cache object. This can be `"app"`
#' (the default), `"session"`, or a cache object like a
#' [cachem::cache_disk()]. See the Cache Scoping section for more information.
#'
#' @seealso [bindEvent()], [renderCachedPlot()] for caching plots.
#'
#' @examples
#' \dontrun{
#' rc <- bindCache(
#' x = reactive({
#' Sys.sleep(2) # Pretend this is expensive
#' input$x * 100
#' }),
#' input$x
#' )
#'
#' # Can make it prettier with the %>% operator
#' library(magrittr)
#'
#' rc <- reactive({
#' Sys.sleep(2)
#' input$x * 100
#' }) %>%
#' bindCache(input$x)
#'
#' }
#'
#' ## Only run app examples in interactive R sessions
#' if (interactive()) {
#'
#' # Basic example
#' shinyApp(
#' ui = fluidPage(
#' sliderInput("x", "x", 1, 10, 5),
#' sliderInput("y", "y", 1, 10, 5),
#' div("x * y: "),
#' verbatimTextOutput("txt")
#' ),
#' server = function(input, output) {
#' r <- reactive({
#' # The value expression is an _expensive_ computation
#' message("Doing expensive computation...")
#' Sys.sleep(2)
#' input$x * input$y
#' }) %>%
#' bindCache(input$x, input$y)
#'
#' output$txt <- renderText(r())
#' }
#' )
#'
#'
#' # Caching renderText
#' shinyApp(
#' ui = fluidPage(
#' sliderInput("x", "x", 1, 10, 5),
#' sliderInput("y", "y", 1, 10, 5),
#' div("x * y: "),
#' verbatimTextOutput("txt")
#' ),
#' server = function(input, output) {
#' output$txt <- renderText({
#' message("Doing expensive computation...")
#' Sys.sleep(2)
#' input$x * input$y
#' }) %>%
#' bindCache(input$x, input$y)
#' }
#' )
#'
#'
#' # Demo of using events and caching with an actionButton
#' shinyApp(
#' ui = fluidPage(
#' sliderInput("x", "x", 1, 10, 5),
#' sliderInput("y", "y", 1, 10, 5),
#' actionButton("go", "Go"),
#' div("x * y: "),
#' verbatimTextOutput("txt")
#' ),
#' server = function(input, output) {
#' r <- reactive({
#' message("Doing expensive computation...")
#' Sys.sleep(2)
#' input$x * input$y
#' }) %>%
#' bindCache(input$x, input$y) %>%
#' bindEvent(input$go)
#' # The cached, eventified reactive takes a reactive dependency on
#' # input$go, but doesn't use it for the cache key. It uses input$x and
#' # input$y for the cache key, but doesn't take a reactive dependency on
#' # them, because the reactive dependency is superseded by addEvent().
#'
#' output$txt <- renderText(r())
#' }
#' )
#'
#' }
#'
#' @export
bindCache <- function(x, ..., cache = "app") {
force(cache)
UseMethod("bindCache")
}
#' @export
bindCache.default <- function(x, ...) {
stop("Don't know how to handle object with class ", paste(class(x), collapse = ", "))
}
#' @export
bindCache.reactiveExpr <- function(x, ..., cache = "app") {
check_dots_unnamed()
call_srcref <- get_call_srcref(-1)
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = exprToLabel(substitute(x), "cachedReactive")
)
domain <- reactive_get_domain(x)
# Convert the ... to a function that returns their evaluated values.
keyFunc <- quos_to_func(enquos0(...))
valueFunc <- reactive_get_value_func(x)
# Hash cache hint now -- this will be added to the key later on, to reduce the
# chance of key collisions with other cachedReactives.
cacheHint <- rlang::hash(extractCacheHint(x))
valueFunc <- wrapFunctionLabel(valueFunc, "cachedReactiveValueFunc", ..stacktraceon = TRUE)
x_classes <- class(x)
x_otel_attrs <- attr(x, "observable", exact = TRUE)$.otelAttrs
# Don't hold on to the reference for x, so that it can be GC'd
rm(x)
# Hacky workaround for issue with `%>%` preventing GC:
# https://github.com/tidyverse/magrittr/issues/229
if (exists(".GenericCallEnv") && exists(".", envir = .GenericCallEnv, inherits = FALSE)) {
rm(list = ".", envir = .GenericCallEnv, inherits = FALSE)
}
with_no_otel_collect({
res <- reactive(label = label, domain = domain, {
cache <- resolve_cache_object(cache, domain)
hybrid_chain(
keyFunc(),
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook = identity, cacheWriteHook = identity)
)
})
})
class(res) <- c("reactive.cache", class(res))
local({
impl <- attr(res, "observable", exact = TRUE)
impl$.otelAttrs <- append_otel_srcref_attrs(x_otel_attrs, call_srcref, fn_name = "bindCache")
})
if (has_otel_collect("reactivity")) {
res <- enable_otel_reactive_expr(res)
}
res
}
#' @export
bindCache.shiny.render.function <- function(x, ..., cache = "app") {
check_dots_unnamed()
keyFunc <- quos_to_func(enquos0(...))
cacheHint <- rlang::hash(extractCacheHint(x))
cacheWriteHook <- attr(x, "cacheWriteHook", exact = TRUE) %||% identity
cacheReadHook <- attr(x, "cacheReadHook", exact = TRUE) %||% identity
valueFunc <- x
renderFunc <- function(...) {
domain <- getDefaultReactiveDomain()
cache <- resolve_cache_object(cache, domain)
hybrid_chain(
keyFunc(),
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook, cacheWriteHook, ...)
)
}
# Passes over the otelAttrs from valueFunc to renderFunc
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
class(renderFunc) <- c("shiny.render.function.cache", class(valueFunc))
renderFunc
}
#' @export
bindCache.shiny.renderPlot <- function(x, ...,
cache = "app",
sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2))
{
check_dots_unnamed()
valueFunc <- x
# Given the actual width/height of the image element in the browser, the
# resize observer computes the width/height using sizePolicy() and pushes
# those values into `fitWidth` and `fitHeight`. It's done this way so that the
# `fitWidth` and `fitHeight` only change (and cause invalidations of the key
# expression) when the rendered image size changes, and not every time the
# browser's <img> tag changes size.
#
# If the key expression were invalidated every time the image element changed
# size, even if the resulting key was the same (because `sizePolicy()` gave
# the same output for a slightly different img element size), it would result
# in getting the (same) image from the cache and sending it to the client
# again. This resize observer prevents that.
fitDims <- reactiveVal(NULL)
resizeObserverCreated <- FALSE
outputName <- NULL
ensureResizeObserver <- function() {
if (resizeObserverCreated)
return()
doResizeCheck <- function() {
if (is.null(outputName)) {
outputName <<- getCurrentOutputInfo()$name
}
session <- getDefaultReactiveDomain()
width <- session$clientData[[paste0('output_', outputName, '_width')]] %||% 0
height <- session$clientData[[paste0('output_', outputName, '_height')]] %||% 0
rect <- sizePolicy(c(width, height))
fitDims(list(width = rect[1], height = rect[2]))
}
# Run it once immediately, then set up the observer
isolate(doResizeCheck())
observe({
doResizeCheck()
}, label = "plot-resize")
# TODO: Make sure this observer gets GC'd if output$foo is replaced.
# Currently, if you reassign output$foo, the observer persists until the
# session ends. This is generally bad programming practice and should be
# rare, but still, we should try to clean up properly.
resizeObserverCreated <<- TRUE
}
renderFunc <- function(...) {
hybrid_chain(
# Pass in fitDims so that so that the generated plot will be the
# dimensions specified by the sizePolicy; otherwise the plot would be the
# exact dimensions of the img element, which isn't what we want for cached
# plots.
valueFunc(..., get_dims = fitDims),
function(img) {
# Replace exact pixel dimensions; instead, the max-height and max-width
# will be set to 100% from CSS.
img$class <- "shiny-scalable"
img$width <- NULL
img$height <- NULL
img
}
)
}
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
class(renderFunc) <- class(valueFunc)
bindCache.shiny.render.function(
renderFunc,
...,
{
ensureResizeObserver()
session <- getDefaultReactiveDomain()
if (is.null(session) || is.null(fitDims())) {
req(FALSE)
}
pixelratio <- session$clientData$pixelratio %||% 1
list(fitDims(), pixelratio)
},
cache = cache
)
}
#' @export
bindCache.reactive.cache <- function(x, ...) {
stop("bindCache() has already been called on the object.")
}
#' @export
bindCache.shiny.render.function.cache <- bindCache.reactive.cache
#' @export
bindCache.reactive.event <- function(x, ...) {
stop("Can't call bindCache() after calling bindEvent() on an object. Maybe you wanted to call bindEvent() after bindCache()?")
}
#' @export
bindCache.shiny.render.function.event <- bindCache.reactive.event
#' @export
bindCache.Observer <- function(x, ...) {
stop("Can't bindCache an observer, because observers exist for the side efects, not for their return values.")
}
#' @export
bindCache.function <- function(x, ...) {
stop(
"Don't know how to add caching to a plain function. ",
"If this is a render* function for Shiny, it may need to be updated. ",
"Please see ?shiny::bindCache for more information."
)
}
# Returns a function which should be passed as a step in to hybrid_chain(). The
# returned function takes a cache key as input and manages storing and retrieving
# values from the cache, as well as executing the valueFunc if needed.
generateCacheFun <- function(
valueFunc,
cache,
cacheHint,
cacheReadHook,
cacheWriteHook,
...
) {
function(cacheKeyResult) {
key_str <- rlang::hash(list(cacheKeyResult, cacheHint))
res <- cache$get(key_str)
# Case 1: cache hit
if (!is.key_missing(res)) {
return(hybrid_chain(
{
# The first step is just to convert `res` to a promise or not, so
# that hybrid_chain() knows to propagate the promise-ness.
if (res$is_promise) promise_resolve(res)
else res
},
function(res) {
if (res$error) {
stop(res$value)
}
cacheReadHook(valueWithVisible(res))
}
))
}
# Case 2: cache miss
#
# valueFunc() might return a promise, or an actual value. Normally we'd
# use a hybrid_chain() for this, but in this case, we need to have
# different behavior if it's a promise or not a promise -- the
# information about whether or not it's a promise needs to be stored in
# the cache. We need to handle both cases and record in the cache
# whether it's a promise or not, so that any consumer of the
# cachedReactive() will be given the correct kind of object (a promise
# vs. an actual value) in the case of a future cache hit.
p <- withCallingHandlers(
withVisible(isolate(valueFunc(...))),
error = function(e) {
cache$set(key_str, list(
is_promise = FALSE,
value = e,
visible = TRUE,
error = TRUE
))
}
)
if (is.promising(p$value)) {
p$value <- as.promise(p$value)
p$value <- p$value$
then(function(value) {
res <- withVisible(value)
cache$set(key_str, list(
is_promise = TRUE,
value = cacheWriteHook(res$value),
visible = res$visible,
error = FALSE
))
valueWithVisible(res)
})$
catch(function(e) {
cache$set(key_str, list(
is_promise = TRUE,
value = e,
visible = TRUE,
error = TRUE
))
stop(e)
})
valueWithVisible(p)
} else {
# result is an ordinary value, not a promise.
cache$set(key_str, list(
is_promise = FALSE,
value = cacheWriteHook(p$value),
visible = p$visible,
error = FALSE
))
return(valueWithVisible(p))
}
}
}
extractCacheHint <- function(func) {
cacheHint <- attr(func, "cacheHint", exact = TRUE)
if (is_false(cacheHint)) {
stop(
"Cannot call `bindCache()` on this object because it is marked as not cacheable.",
call. = FALSE
)
}
if (is.null(cacheHint)) {
warning("No cacheHint found for this object. ",
"Caching may not work properly.")
}
cacheHint
}

360
R/bind-event.R Normal file
View File

@@ -0,0 +1,360 @@
#' Make an object respond only to specified reactive events
#'
#' @description
#'
#' Modify an object to respond to "event-like" reactive inputs, values, and
#' expressions. `bindEvent()` can be used with reactive expressions, render
#' functions, and observers. The resulting object takes a reactive dependency on
#' the `...` arguments, and not on the original object's code. This can, for
#' example, be used to make an observer execute only when a button is pressed.
#'
#' `bindEvent()` was added in Shiny 1.6.0. When it is used with [reactive()] and
#' [observe()], it does the same thing as [eventReactive()] and
#' [observeEvent()]. However, `bindEvent()` is more flexible: it can be combined
#' with [bindCache()], and it can also be used with `render` functions (like
#' [renderText()] and [renderPlot()]).
#'
#' @section Details:
#'
#' Shiny's reactive programming framework is primarily designed for calculated
#' values (reactive expressions) and side-effect-causing actions (observers)
#' that respond to *any* of their inputs changing. That's often what is
#' desired in Shiny apps, but not always: sometimes you want to wait for a
#' specific action to be taken from the user, like clicking an
#' [actionButton()], before calculating an expression or taking an action. A
#' reactive value or expression that is used to trigger other calculations in
#' this way is called an *event*.
#'
#' These situations demand a more imperative, "event handling" style of
#' programming that is possible--but not particularly intuitive--using the
#' reactive programming primitives [observe()] and [isolate()]. `bindEvent()`
#' provides a straightforward API for event handling that wraps `observe` and
#' `isolate`.
#'
#' The `...` arguments are captured as expressions and combined into an
#' **event expression**. When this event expression is invalidated (when its
#' upstream reactive inputs change), that is an **event**, and it will cause
#' the original object's code to execute.
#'
#' Use `bindEvent()` with `observe()` whenever you want to *perform an action*
#' in response to an event. (This does the same thing as [observeEvent()],
#' which was available in Shiny prior to version 1.6.0.) Note that
#' "recalculate a value" does not generally count as performing an action --
#' use [reactive()] for that.
#'
#' Use `bindEvent()` with `reactive()` to create a *calculated value* that
#' only updates in response to an event. This is just like a normal [reactive
#' expression][reactive] except it ignores all the usual invalidations that
#' come from its reactive dependencies; it only invalidates in response to the
#' given event. (This does the same thing as [eventReactive()], which was
#' available in Shiny prior to version 1.6.0.)
#'
#' `bindEvent()` is often used with [bindCache()].
#'
#' @section ignoreNULL and ignoreInit:
#'
#' `bindEvent()` takes an `ignoreNULL` parameter that affects behavior when
#' the event expression evaluates to `NULL` (or in the special case of an
#' [actionButton()], `0`). In these cases, if `ignoreNULL` is `TRUE`, then it
#' will raise a silent [validation][validate] error. This is useful behavior
#' if you don't want to do the action or calculation when your app first
#' starts, but wait for the user to initiate the action first (like a "Submit"
#' button); whereas `ignoreNULL=FALSE` is desirable if you want to initially
#' perform the action/calculation and just let the user re-initiate it (like a
#' "Recalculate" button).
#'
#' `bindEvent()` also takes an `ignoreInit` argument. By default, reactive
#' expressions and observers will run on the first reactive flush after they
#' are created (except if, at that moment, the event expression evaluates to
#' `NULL` and `ignoreNULL` is `TRUE`). But when responding to a click of an
#' action button, it may often be useful to set `ignoreInit` to `TRUE`. For
#' example, if you're setting up an observer to respond to a dynamically
#' created button, then `ignoreInit = TRUE` will guarantee that the action
#' will only be triggered when the button is actually clicked, instead of also
#' being triggered when it is created/initialized. Similarly, if you're
#' setting up a reactive that responds to a dynamically created button used to
#' refresh some data (which is then returned by that `reactive`), then you
#' should use `reactive(...) %>% bindEvent(..., ignoreInit = TRUE)` if you
#' want to let the user decide if/when they want to refresh the data (since,
#' depending on the app, this may be a computationally expensive operation).
#'
#' Even though `ignoreNULL` and `ignoreInit` can be used for similar purposes
#' they are independent from one another. Here's the result of combining
#' these:
#'
#' \describe{
#' \item{`ignoreNULL = TRUE` and `ignoreInit = FALSE`}{
#' This is the default. This combination means that reactive/observer code
#' will run every time that event expression is not
#' `NULL`. If, at the time of creation, the event expression happens
#' to *not* be `NULL`, then the code runs.
#' }
#' \item{`ignoreNULL = FALSE` and `ignoreInit = FALSE`}{
#' This combination means that reactive/observer code will
#' run every time no matter what.
#' }
#' \item{`ignoreNULL = FALSE` and `ignoreInit = TRUE`}{
#' This combination means that reactive/observer code will
#' *not* run at the time of creation (because `ignoreInit = TRUE`),
#' but it will run every other time.
#' }
#' \item{`ignoreNULL = TRUE` and `ignoreInit = TRUE`}{
#' This combination means that reactive/observer code will
#' *not* at the time of creation (because `ignoreInit = TRUE`).
#' After that, the reactive/observer code will run every time that
#' the event expression is not `NULL`.
#' }
#' }
#'
#' @section Types of objects:
#'
#' `bindEvent()` can be used with reactive expressions, observers, and shiny
#' render functions.
#'
#' When `bindEvent()` is used with `reactive()`, it creates a new reactive
#' expression object.
#'
#' When `bindEvent()` is used with `observe()`, it alters the observer in
#' place. It can only be used with observers which have not yet executed.
#'
#' @section Combining events and caching:
#'
#' In many cases, it makes sense to use `bindEvent()` along with
#' `bindCache()`, because they each can reduce the amount of work done on the
#' server. For example, you could have [sliderInput]s `x` and `y` and a
#' `reactive()` that performs a time-consuming operation with those values.
#' Using `bindCache()` can speed things up, especially if there are multiple
#' users. But it might make sense to also not do the computation until the
#' user sets both `x` and `y`, and then clicks on an [actionButton] named
#' `go`.
#'
#' To use both caching and events, the object should first be passed to
#' `bindCache()`, then `bindEvent()`. For example:
#'
#' ```
#' r <- reactive({
#' Sys.sleep(2) # Pretend this is an expensive computation
#' input$x * input$y
#' }) %>%
#' bindCache(input$x, input$y) %>%
#' bindEvent(input$go)
#' ```
#'
#' Anything that consumes `r()` will take a reactive dependency on the event
#' expression given to `bindEvent()`, and not the cache key expression given to
#' `bindCache()`. In this case, it is just `input$go`.
#'
#' @param x An object to wrap so that is triggered only when a the specified
#' event occurs.
#' @param ignoreNULL Whether the action should be triggered (or value
#' calculated) when the input is `NULL`. See Details.
#' @param ignoreInit If `TRUE`, then, when the eventified object is first
#' created/initialized, don't trigger the action or (compute the value). The
#' default is `FALSE`. See Details.
#' @param once Used only for observers. Whether this `observer` should be
#' immediately destroyed after the first time that the code in the observer is
#' run. This pattern is useful when you want to subscribe to a event that
#' should only happen once.
#' @param label A label for the observer or reactive, useful for debugging.
#' @param ... One or more expressions that represents the event; this can be a
#' simple reactive value like `input$click`, a call to a reactive expression
#' like `dataset()`, or even a complex expression inside curly braces. If
#' there are multiple expressions in the `...`, then it will take a dependency
#' on all of them.
#' @export
bindEvent <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
once = FALSE, label = NULL)
{
check_dots_unnamed()
force(ignoreNULL)
force(ignoreInit)
force(once)
UseMethod("bindEvent")
}
#' @export
bindEvent.default <- function(x, ...) {
stop("Don't know how to handle object with class ", paste(class(x), collapse = ", "))
}
#' @export
bindEvent.reactiveExpr <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
label = NULL)
{
domain <- reactive_get_domain(x)
qs <- enquos0(...)
eventFunc <- quos_to_func(qs)
valueFunc <- reactive_get_value_func(x)
valueFunc <- wrapFunctionLabel(valueFunc, "eventReactiveValueFunc", ..stacktraceon = TRUE)
call_srcref <- get_call_srcref(-1)
if (is.null(label)) {
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = as_default_label(sprintf(
'bindEvent(%s, %s)',
attr(x, "observable", exact = TRUE)$.label,
quos_to_label(qs)
))
)
}
x_classes <- class(x)
x_otel_attrs <- attr(x, "observable", exact = TRUE)$.otelAttrs
# Don't hold on to the reference for x, so that it can be GC'd
rm(x)
initialized <- FALSE
with_no_otel_collect({
res <- reactive(label = label, domain = domain, ..stacktraceon = FALSE, {
hybrid_chain(
{
eventFunc()
},
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
req(FALSE)
}
req(!ignoreNULL || !isNullEvent(value))
isolate(valueFunc())
}
)
})
})
class(res) <- c("reactive.event", x_classes)
local({
impl <- attr(res, "observable", exact = TRUE)
impl$.otelAttrs <- append_otel_srcref_attrs(x_otel_attrs, call_srcref, fn_name = "bindEvent")
})
if (has_otel_collect("reactivity")) {
res <- enable_otel_reactive_expr(res)
}
res
}
#' @export
bindEvent.shiny.render.function <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE) {
eventFunc <- quos_to_func(enquos0(...))
valueFunc <- x
initialized <- FALSE
renderFunc <- function(...) {
hybrid_chain(
eventFunc(),
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
req(FALSE)
}
req(!ignoreNULL || !isNullEvent(value))
isolate(valueFunc(...))
}
)
}
# Passes over the otelAttrs from valueFunc to renderFunc
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
class(renderFunc) <- c("shiny.render.function.event", class(valueFunc))
renderFunc
}
#' @export
bindEvent.Observer <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
once = FALSE, label = NULL)
{
if (x$.execCount > 0) {
stop("Cannot call bindEvent() on an Observer that has already been executed.")
}
qs <- enquos0(...)
eventFunc <- quos_to_func(qs)
valueFunc <- x$.func
# Note that because the observer will already have been logged by this point,
# this updated label won't show up in the reactlog.
if (is.null(label)) {
call_srcref <- get_call_srcref(-1)
x$.label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = as_default_label(
sprintf('bindEvent(%s, %s)', x$.label, quos_to_label(qs))
)
)
} else {
x$.label <- label
}
initialized <- FALSE
x$.func <- wrapFunctionLabel(
name = x$.label,
..stacktraceon = FALSE,
func = function() {
hybrid_chain(
eventFunc(),
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
return()
}
if (ignoreNULL && isNullEvent(value)) {
return()
}
if (once) {
on.exit(x$destroy())
}
req(!ignoreNULL || !isNullEvent(value))
isolate(valueFunc())
}
)
}
)
class(x) <- c("Observer.event", class(x))
call_srcref <- get_call_srcref(-1)
x$.otelAttrs <- append_otel_srcref_attrs(x$.otelAttrs, call_srcref, fn_name = "bindEvent")
if (has_otel_collect("reactivity")) {
x <- enable_otel_observe(x)
}
invisible(x)
}
#' @export
bindEvent.reactive.event <- function(x, ...) {
stop("bindEvent() has already been called on the object.")
}
#' @export
bindEvent.Observer.event <- bindEvent.reactive.event

View File

@@ -1,6 +1,3 @@
#' @include stack.R
NULL
ShinySaveState <- R6Class("ShinySaveState",
public = list(
input = NULL,
@@ -79,7 +76,7 @@ saveShinySaveState <- function(state) {
# Look for a save.interface function. This will be defined by the hosting
# environment if it supports bookmarking.
saveInterface <- getShinyOption("save.interface")
saveInterface <- getShinyOption("save.interface", default = NULL)
if (is.null(saveInterface)) {
if (inShinyServer()) {
@@ -102,13 +99,13 @@ saveShinySaveState <- function(state) {
# Encode the state to a URL. This does not save to disk.
encodeShinySaveState <- function(state) {
exclude <- c(state$exclude, "._bookmark_")
inputVals <- serializeReactiveValues(state$input, exclude, stateDir = NULL)
# Allow user-supplied onSave function to do things like add state$values.
if (!is.null(state$onSave))
isolate(state$onSave(state))
exclude <- c(state$exclude, "._bookmark_")
inputVals <- serializeReactiveValues(state$input, exclude, stateDir = NULL)
inputVals <- vapply(inputVals,
function(x) toJSON(x, strict_atomic = FALSE),
character(1),
@@ -217,6 +214,22 @@ RestoreContext <- R6Class("RestoreContext",
self$dir <- NULL
},
# Completely replace the state
set = function(active = FALSE, initErrorMessage = NULL, input = list(), values = list(), dir = NULL) {
# Validate all inputs
stopifnot(is.logical(active))
stopifnot(is.null(initErrorMessage) || is.character(initErrorMessage))
stopifnot(is.list(input))
stopifnot(is.list(values))
stopifnot(is.null(dir) || is.character(dir))
self$active <- active
self$initErrorMessage <- initErrorMessage
self$input <- RestoreInputSet$new(input)
self$values <- list2env2(values, parent = emptyenv())
self$dir <- dir
},
# This should be called before a restore context is popped off the stack.
flushPending = function() {
self$input$flushPending()
@@ -280,7 +293,7 @@ RestoreContext <- R6Class("RestoreContext",
# Look for a load.interface function. This will be defined by the hosting
# environment if it supports bookmarking.
loadInterface <- getShinyOption("load.interface")
loadInterface <- getShinyOption("load.interface", default = NULL)
if (is.null(loadInterface)) {
if (inShinyServer()) {
@@ -308,34 +321,38 @@ RestoreContext <- R6Class("RestoreContext",
if (substr(queryString, 1, 1) == '?')
queryString <- substr(queryString, 2, nchar(queryString))
# The "=" after "_inputs_" is optional. Shiny doesn't generate URLs with
# "=", but httr always adds "=".
inputs_reg <- "(^|&)_inputs_=?(&|$)"
values_reg <- "(^|&)_values_=?(&|$)"
# Error if multiple '_inputs_' or '_values_'. This is needed because
# strsplit won't add an entry if the search pattern is at the end of a
# string.
if (length(gregexpr("(^|&)_inputs_(&|$)", queryString)[[1]]) > 1)
if (length(gregexpr(inputs_reg, queryString)[[1]]) > 1)
stop("Invalid state string: more than one '_inputs_' found")
if (length(gregexpr("(^|&)_values_(&|$)", queryString)[[1]]) > 1)
if (length(gregexpr(values_reg, queryString)[[1]]) > 1)
stop("Invalid state string: more than one '_values_' found")
# Look for _inputs_ and store following content in inputStr
splitStr <- strsplit(queryString, "(^|&)_inputs_(&|$)")[[1]]
splitStr <- strsplit(queryString, inputs_reg)[[1]]
if (length(splitStr) == 2) {
inputStr <- splitStr[2]
# Remove any _values_ (and content after _values_) that may come after
# _inputs_
inputStr <- strsplit(inputStr, "(^|&)_values_(&|$)")[[1]][1]
inputStr <- strsplit(inputStr, values_reg)[[1]][1]
} else {
inputStr <- ""
}
# Look for _values_ and store following content in valueStr
splitStr <- strsplit(queryString, "(^|&)_values_(&|$)")[[1]]
splitStr <- strsplit(queryString, values_reg)[[1]]
if (length(splitStr) == 2) {
valueStr <- splitStr[2]
# Remove any _inputs_ (and content after _inputs_) that may come after
# _values_
valueStr <- strsplit(valueStr, "(^|&)_inputs_(&|$)")[[1]][1]
valueStr <- strsplit(valueStr, inputs_reg)[[1]][1]
} else {
valueStr <- ""
@@ -346,16 +363,20 @@ RestoreContext <- R6Class("RestoreContext",
values <- parseQueryString(valueStr, nested = TRUE)
valuesFromJSON <- function(vals) {
mapply(names(vals), vals, SIMPLIFY = FALSE,
varsUnparsed <- c()
valsParsed <- mapply(names(vals), vals, SIMPLIFY = FALSE,
FUN = function(name, value) {
tryCatch(
safeFromJSON(value),
error = function(e) {
stop("Failed to parse URL parameter \"", name, "\"")
varsUnparsed <<- c(varsUnparsed, name)
warning("Failed to parse URL parameter \"", name, "\"")
}
)
}
)
valsParsed[varsUnparsed] <- NULL
valsParsed
}
inputs <- valuesFromJSON(inputs)
@@ -431,8 +452,10 @@ RestoreInputSet <- R6Class("RestoreInputSet",
)
)
restoreCtxStack <- Stack$new()
restoreCtxStack <- NULL
on_load({
restoreCtxStack <- fastmap::faststack()
})
withRestoreContext <- function(ctx, expr) {
restoreCtxStack$push(ctx)
@@ -453,7 +476,7 @@ hasCurrentRestoreContext <- function() {
domain <- getDefaultReactiveDomain()
if (!is.null(domain) && !is.null(domain$restoreContext))
return(TRUE)
return(FALSE)
}
@@ -528,7 +551,7 @@ restoreInput <- function(id, default) {
#' `window.history.pushState(null, null, queryString)`.
#'
#' @param queryString The new query string to show in the location bar.
#' @param mode When the query string is updated, should the the current history
#' @param mode When the query string is updated, should the current history
#' entry be replaced (default), or should a new history entry be pushed onto
#' the history stack? The former should only be used in a live bookmarking
#' context. The latter is useful if you want to navigate between states using
@@ -1144,10 +1167,10 @@ setBookmarkExclude <- function(names = character(0), session = getDefaultReactiv
#' toupper(input$text)
#' })
#' onBookmark(function(state) {
#' state$values$hash <- digest::digest(input$text, "md5")
#' state$values$hash <- rlang::hash(input$text)
#' })
#' onRestore(function(state) {
#' if (identical(digest::digest(input$text, "md5"), state$values$hash)) {
#' if (identical(rlang::hash(input$text), state$values$hash)) {
#' message("Module's input text matches hash ", state$values$hash)
#' } else {
#' message("Module's input text does not match hash ", state$values$hash)
@@ -1170,10 +1193,10 @@ setBookmarkExclude <- function(names = character(0), session = getDefaultReactiv
#' server <- function(input, output, session) {
#' callModule(capitalizerServer, "tc")
#' onBookmark(function(state) {
#' state$values$hash <- digest::digest(input$text, "md5")
#' state$values$hash <- rlang::hash(input$text)
#' })
#' onRestore(function(state) {
#' if (identical(digest::digest(input$text, "md5"), state$values$hash)) {
#' if (identical(rlang::hash(input$text), state$values$hash)) {
#' message("App's input text matches hash ", state$values$hash)
#' } else {
#' message("App's input text does not match hash ", state$values$hash)

View File

@@ -6,7 +6,7 @@
#' @param sidebarPanel The [sidebarPanel] containing input controls
#' @param mainPanel The [mainPanel] containing outputs
#' @keywords internal
#' @return A UI defintion that can be passed to the [shinyUI] function
#' @return A UI definition that can be passed to the [shinyUI] function
#' @export
pageWithSidebar <- function(headerPanel,
sidebarPanel,

View File

@@ -11,13 +11,9 @@
#' @param ... Elements to include within the page
#' @param title The browser window title (defaults to the host URL of the page).
#' Can also be set as a side effect of the [titlePanel()] function.
#' @param responsive This option is deprecated; it is no longer optional with
#' Bootstrap 3.
#' @param theme Alternative Bootstrap stylesheet (normally a css file within the
#' www directory). For example, to use the theme located at
#' `www/bootstrap.css` you would use `theme = "bootstrap.css"`.
#' @inheritParams bootstrapPage
#'
#' @return A UI defintion that can be passed to the [shinyUI] function.
#' @return A UI definition that can be passed to the [shinyUI] function.
#'
#' @details To create a fluid page use the `fluidPage` function and include
#' instances of `fluidRow` and [column()] within it. As an
@@ -25,7 +21,7 @@
#' higher-level layout functions like [sidebarLayout()].
#'
#' @note See the [
#' Shiny-Application-Layout-Guide](http://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fluid
#' Shiny-Application-Layout-Guide](https://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fluid
#' pages.
#'
#' @family layout functions
@@ -87,11 +83,11 @@
#' }
#' @rdname fluidPage
#' @export
fluidPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
fluidPage <- function(..., title = NULL, theme = NULL, lang = NULL) {
bootstrapPage(div(class = "container-fluid", ...),
title = title,
responsive = responsive,
theme = theme)
theme = theme,
lang = lang)
}
@@ -113,13 +109,9 @@ fluidRow <- function(...) {
#'
#' @param ... Elements to include within the container
#' @param title The browser window title (defaults to the host URL of the page)
#' @param responsive This option is deprecated; it is no longer optional with
#' Bootstrap 3.
#' @param theme Alternative Bootstrap stylesheet (normally a css file within the
#' www directory). For example, to use the theme located at
#' `www/bootstrap.css` you would use `theme = "bootstrap.css"`.
#' @inheritParams bootstrapPage
#'
#' @return A UI defintion that can be passed to the [shinyUI] function.
#' @return A UI definition that can be passed to the [shinyUI] function.
#'
#' @details To create a fixed page use the `fixedPage` function and include
#' instances of `fixedRow` and [column()] within it. Note that
@@ -128,7 +120,7 @@ fluidRow <- function(...) {
#' with `fixedRow` and `column`.
#'
#' @note See the [
#' Shiny Application Layout Guide](http://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fixed
#' Shiny Application Layout Guide](https://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fixed
#' pages.
#'
#' @family layout functions
@@ -156,11 +148,11 @@ fluidRow <- function(...) {
#'
#' @rdname fixedPage
#' @export
fixedPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
fixedPage <- function(..., title = NULL, theme = NULL, lang = NULL) {
bootstrapPage(div(class = "container", ...),
title = title,
responsive = responsive,
theme = theme)
theme = theme,
lang = lang)
}
#' @rdname fixedPage
@@ -355,6 +347,8 @@ sidebarLayout <- function(sidebarPanel,
sidebarPanel <- function(..., width = 4) {
div(class=paste0("col-sm-", width),
tags$form(class="well",
# A11y semantic landmark for sidebar
role="complementary",
...
)
)
@@ -364,6 +358,8 @@ sidebarPanel <- function(..., width = 4) {
#' @rdname sidebarLayout
mainPanel <- function(..., width = 8) {
div(class=paste0("col-sm-", width),
# A11y semantic landmark for main region
role="main",
...
)
}
@@ -394,7 +390,7 @@ mainPanel <- function(..., width = 8) {
#' }
#' @export
verticalLayout <- function(..., fluid = TRUE) {
lapply(list(...), function(row) {
lapply(list2(...), function(row) {
col <- column(12, row)
if (fluid)
fluidRow(col)
@@ -431,8 +427,8 @@ verticalLayout <- function(..., fluid = TRUE) {
#' @export
flowLayout <- function(..., cellArgs = list()) {
children <- list(...)
childIdx <- !nzchar(names(children) %OR% character(length(children)))
children <- list2(...)
childIdx <- !nzchar(names(children) %||% character(length(children)))
attribs <- children[!childIdx]
children <- children[childIdx]
@@ -514,13 +510,13 @@ inputPanel <- function(...) {
#' @export
splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
children <- list(...)
childIdx <- !nzchar(names(children) %OR% character(length(children)))
children <- list2(...)
childIdx <- !nzchar(names(children) %||% character(length(children)))
attribs <- children[!childIdx]
children <- children[childIdx]
count <- length(children)
if (length(cellWidths) == 0 || is.na(cellWidths)) {
if (length(cellWidths) == 0 || isTRUE(is.na(cellWidths))) {
cellWidths <- sprintf("%.3f%%", 100 / count)
}
cellWidths <- rep(cellWidths, length.out = count)
@@ -612,7 +608,7 @@ fillCol <- function(..., flex = 1, width = "100%", height = "100%") {
}
flexfill <- function(..., direction, flex, width = width, height = height) {
children <- list(...)
children <- list2(...)
attrs <- list()
if (!is.null(names(children))) {
@@ -693,37 +689,3 @@ flexfill <- function(..., direction, flex, width = width, height = height) {
)
do.call(tags$div, c(attrs, divArgs))
}
css <- function(..., collapse_ = "") {
props <- list(...)
if (length(props) == 0) {
return("")
}
if (is.null(names(props)) || any(names(props) == "")) {
stop("cssList expects all arguments to be named")
}
# Necessary to make factors show up as level names, not numbers
props[] <- lapply(props, paste, collapse = " ")
# Drop null args
props <- props[!sapply(props, empty)]
if (length(props) == 0) {
return("")
}
# Replace all '.' and '_' in property names to '-'
names(props) <- gsub("[._]", "-", tolower(gsub("([A-Z])", "-\\1", names(props))))
# Create "!important" suffix for each property whose name ends with !, then
# remove the ! from the property name
important <- ifelse(grepl("!$", names(props), perl = TRUE), " !important", "")
names(props) <- sub("!$", "", names(props), perl = TRUE)
paste0(names(props), ":", props, important, ";", collapse = collapse_)
}
empty <- function(x) {
length(x) == 0 || (is.character(x) && !any(nzchar(x)))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
# Generated by tools/updateSpinnerTypes.R: do not edit by hand
.busySpinnerTypes <-
c("ring", "ring2", "ring3", "bars", "bars2", "bars3", "pulse",
"pulse2", "pulse3", "dots", "dots2", "dots3")

294
R/busy-indicators.R Normal file
View File

@@ -0,0 +1,294 @@
#' Enable/disable busy indication
#'
#' Busy indicators provide a visual cue to users when the server is busy
#' calculating outputs or otherwise performing tasks (e.g., producing
#' downloads). When enabled, a spinner is shown on each
#' calculating/recalculating output, and a pulsing banner is shown at the top of
#' the page when the app is otherwise busy. Busy indication is enabled by
#' default for UI created with \pkg{bslib}, but must be enabled otherwise. To
#' enable/disable, include the result of this function in anywhere in the app's
#' UI.
#'
#' When both `spinners` and `pulse` are set to `TRUE`, the pulse is
#' automatically disabled when spinner(s) are active. When both `spinners` and
#' `pulse` are set to `FALSE`, no busy indication is shown (other than the
#' graying out of recalculating outputs).
#'
#' @param ... Currently ignored.
#' @param spinners Whether to show a spinner on each calculating/recalculating
#' output.
#' @param pulse Whether to show a pulsing banner at the top of the page when the
#' app is busy.
#' @param fade Whether to fade recalculating outputs. A value of `FALSE` is
#' equivalent to `busyIndicatorOptions(fade_opacity=1)`.
#'
#' @export
#' @seealso [busyIndicatorOptions()] for customizing the appearance of the busy
#' indicators.
#' @examplesIf rlang::is_interactive()
#'
#' library(bslib)
#'
#' ui <- page_fillable(
#' useBusyIndicators(),
#' card(
#' card_header(
#' "A plot",
#' input_task_button("simulate", "Simulate"),
#' class = "d-flex justify-content-between align-items-center"
#' ),
#' plotOutput("p"),
#' )
#' )
#'
#' server <- function(input, output) {
#' output$p <- renderPlot({
#' input$simulate
#' Sys.sleep(4)
#' plot(x = rnorm(100), y = rnorm(100))
#' })
#' }
#'
#' shinyApp(ui, server)
useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE, fade = TRUE) {
rlang::check_dots_empty()
attrs <- list("shinyBusySpinners" = spinners, "shinyBusyPulse" = pulse)
js <- vapply(names(attrs), character(1), FUN = function(key) {
if (attrs[[key]]) {
sprintf("document.documentElement.dataset.%s = 'true';", key)
} else {
sprintf("delete document.documentElement.dataset.%s;", key)
}
})
# TODO: it'd be nice if htmltools had something like a page_attrs() that allowed us
# to do this without needing to inject JS into the head.
res <- tags$script(HTML(paste(js, collapse = "\n")))
if (!fade) {
res <- tagList(res, fadeOptions(opacity = 1))
}
res
}
#' Customize busy indicator options
#'
#' @description
#' Shiny automatically includes busy indicators, which more specifically means:
#' 1. Calculating/recalculating outputs have a spinner overlay.
#' 2. Outputs fade out/in when recalculating.
#' 3. When no outputs are calculating/recalculating, but Shiny is busy
#' doing something else (e.g., a download, side-effect, etc), a page-level
#' pulsing banner is shown.
#'
#' This function allows you to customize the appearance of these busy indicators
#' by including the result of this function inside the app's UI. Note that,
#' unless `spinner_selector` (or `fade_selector`) is specified, the spinner/fade
#' customization applies to the parent element. If the customization should
#' instead apply to the entire page, set `spinner_selector = 'html'` and
#' `fade_selector = 'html'`.
#'
#' @param ... Currently ignored.
#' @param spinner_type The type of spinner. Pre-bundled types include:
#' '`r paste0(.busySpinnerTypes, collapse = "', '")`'.
#'
#' A path to a local SVG file can also be provided. The SVG should adhere to
#' the following rules:
#' * The SVG itself should contain the animation.
#' * It should avoid absolute sizes (the spinner's containing DOM element
#' size is set in CSS by `spinner_size`, so it should fill that container).
#' * It should avoid setting absolute colors (the spinner's containing DOM element
#' color is set in CSS by `spinner_color`, so it should inherit that color).
#' @param spinner_color The color of the spinner. This can be any valid CSS
#' color. Defaults to the app's "primary" color if Bootstrap is on the page.
#' @param spinner_size The size of the spinner. This can be any valid CSS size.
#' @param spinner_delay The amount of time to wait before showing the spinner.
#' This can be any valid CSS time and can be useful for not showing the spinner
#' if the computation finishes quickly.
#' @param spinner_selector A character string containing a CSS selector for
#' scoping the spinner customization. The default (`NULL`) will apply the
#' spinner customization to the parent element of the spinner.
#' @param fade_opacity The opacity (a number between 0 and 1) for recalculating
#' output. Set to 1 to "disable" the fade.
#' @param fade_selector A character string containing a CSS selector for
#' scoping the spinner customization. The default (`NULL`) will apply the
#' spinner customization to the parent element of the spinner.
#' @param pulse_background A CSS background definition for the pulse. The
#' default uses a
#' [linear-gradient](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient)
#' of the theme's indigo, purple, and pink colors.
#' @param pulse_height The height of the pulsing banner. This can be any valid
#' CSS size.
#' @param pulse_speed The speed of the pulsing banner. This can be any valid CSS
#' time.
#'
#' @export
#' @seealso [useBusyIndicators()] to disable/enable busy indicators.
#' @examplesIf rlang::is_interactive()
#'
#' library(bslib)
#'
#' card_ui <- function(id, spinner_type = id) {
#' card(
#' busyIndicatorOptions(spinner_type = spinner_type),
#' card_header(paste("Spinner:", spinner_type)),
#' plotOutput(shiny::NS(id, "plot"))
#' )
#' }
#'
#' card_server <- function(id, simulate = reactive()) {
#' moduleServer(
#' id = id,
#' function(input, output, session) {
#' output$plot <- renderPlot({
#' Sys.sleep(1)
#' simulate()
#' plot(x = rnorm(100), y = rnorm(100))
#' })
#' }
#' )
#' }
#'
#' ui <- page_fillable(
#' useBusyIndicators(),
#' input_task_button("simulate", "Simulate", icon = icon("refresh")),
#' layout_columns(
#' card_ui("ring"),
#' card_ui("bars"),
#' card_ui("dots"),
#' card_ui("pulse"),
#' col_widths = 6
#' )
#' )
#'
#' server <- function(input, output, session) {
#' simulate <- reactive(input$simulate)
#' card_server("ring", simulate)
#' card_server("bars", simulate)
#' card_server("dots", simulate)
#' card_server("pulse", simulate)
#' }
#'
#' shinyApp(ui, server)
#'
busyIndicatorOptions <- function(
...,
spinner_type = NULL,
spinner_color = NULL,
spinner_size = NULL,
spinner_delay = NULL,
spinner_selector = NULL,
fade_opacity = NULL,
fade_selector = NULL,
pulse_background = NULL,
pulse_height = NULL,
pulse_speed = NULL
) {
rlang::check_dots_empty()
res <- tagList(
spinnerOptions(
type = spinner_type,
color = spinner_color,
size = spinner_size,
delay = spinner_delay,
selector = spinner_selector
),
fadeOptions(opacity = fade_opacity, selector = fade_selector),
pulseOptions(
background = pulse_background,
height = pulse_height,
speed = pulse_speed
)
)
bslib::as.card_item(dropNulls(res))
}
spinnerOptions <- function(type = NULL, color = NULL, size = NULL, delay = NULL, selector = NULL) {
if (is.null(type) && is.null(color) && is.null(size) && is.null(delay) && is.null(selector)) {
return(NULL)
}
url <- NULL
if (!is.null(type)) {
stopifnot(is.character(type) && length(type) == 1)
if (file.exists(type) && grepl("\\.svg$", type)) {
typeRaw <- readBin(type, "raw", n = file.info(type)$size)
url <- sprintf("url('data:image/svg+xml;base64,%s')", rawToBase64(typeRaw))
} else {
type <- rlang::arg_match(type, .busySpinnerTypes)
url <- sprintf("url('spinners/%s.svg')", type)
}
}
# Options controlled via CSS variables.
css_vars <- htmltools::css(
"--shiny-spinner-url" = url,
"--shiny-spinner-color" = htmltools::parseCssColors(color),
"--shiny-spinner-size" = htmltools::validateCssUnit(size),
"--shiny-spinner-delay" = delay
)
id <- NULL
if (is.null(selector)) {
id <- paste0("spinner-options-", p_randomInt(100, 1000000))
selector <- sprintf(":has(> #%s)", id)
}
css <- HTML(paste0(selector, " {", css_vars, "}"))
tags$style(css, id = id)
}
fadeOptions <- function(opacity = NULL, selector = NULL) {
if (is.null(opacity) && is.null(selector)) {
return(NULL)
}
css_vars <- htmltools::css(
"--shiny-fade-opacity" = opacity
)
id <- NULL
if (is.null(selector)) {
id <- paste0("fade-options-", p_randomInt(100, 1000000))
selector <- sprintf(":has(> #%s)", id)
}
css <- HTML(paste0(selector, " {", css_vars, "}"))
tags$style(css, id = id)
}
pulseOptions <- function(background = NULL, height = NULL, speed = NULL) {
if (is.null(background) && is.null(height) && is.null(speed)) {
return(NULL)
}
css_vars <- htmltools::css(
"--shiny-pulse-background" = background,
"--shiny-pulse-height" = htmltools::validateCssUnit(height),
"--shiny-pulse-speed" = speed
)
tags$style(HTML(paste0(":root {", css_vars, "}")))
}
busyIndicatorDependency <- function() {
htmlDependency(
name = "shiny-busy-indicators",
version = get_package_version("shiny"),
src = "www/shared/busy-indicators",
package = "shiny",
stylesheet = "busy-indicators.css",
# TODO-future: In next release make spinners and pulse opt-out
# head = as.character(useBusyIndicators())
)
}

View File

@@ -1,77 +0,0 @@
# A context object for tracking a cache that needs to be dirtied when a set of
# files changes on disk. Each time the cache is dirtied, the set of files is
# cleared. Therefore, the set of files needs to be re-built each time the cached
# code executes. This approach allows for dynamic dependency graphs.
CacheContext <- R6Class(
'CacheContext',
portable = FALSE,
class = FALSE,
public = list(
.dirty = TRUE,
# List of functions that return TRUE if dirty
.tests = list(),
addDependencyFile = function(file) {
if (.dirty)
return()
file <- normalizePath(file)
mtime <- file.info(file)$mtime
.tests <<- c(.tests, function() {
newMtime <- try(file.info(file)$mtime, silent=TRUE)
if (inherits(newMtime, 'try-error'))
return(TRUE)
return(!identical(mtime, newMtime))
})
invisible()
},
forceDirty = function() {
.dirty <<- TRUE
.tests <<- list()
invisible()
},
isDirty = function() {
if (.dirty)
return(TRUE)
for (test in .tests) {
if (test()) {
forceDirty()
return(TRUE)
}
}
return(FALSE)
},
reset = function() {
.dirty <<- FALSE
.tests <<- list()
},
with = function(func) {
oldCC <- .currentCacheContext$cc
.currentCacheContext$cc <- self
on.exit(.currentCacheContext$cc <- oldCC)
return(func())
}
)
)
.currentCacheContext <- new.env()
# Indicates to Shiny that the given file path is part of the dependency graph
# for whatever is currently executing (so far, only ui.R). By default, ui.R only
# gets re-executed when it is detected to have changed; this function allows the
# caller to indicate that it should also re-execute if the given file changes.
#
# If NULL or NA is given as the argument, then ui.R will re-execute next time.
dependsOnFile <- function(filepath) {
if (is.null(.currentCacheContext$cc))
return()
if (is.null(filepath) || is.na(filepath))
.currentCacheContext$cc$forceDirty()
else
.currentCacheContext$cc$addDependencyFile(filepath)
}

View File

@@ -1,561 +0,0 @@
#' Create a disk cache object
#'
#' A disk cache object is a key-value store that saves the values as files in a
#' directory on disk. Objects can be stored and retrieved using the `get()`
#' and `set()` methods. Objects are automatically pruned from the cache
#' according to the parameters `max_size`, `max_age`, `max_n`,
#' and `evict`.
#'
#'
#' @section Missing Keys:
#'
#' The `missing` and `exec_missing` parameters controls what happens
#' when `get()` is called with a key that is not in the cache (a cache
#' miss). The default behavior is to return a [key_missing()]
#' object. This is a *sentinel value* that indicates that the key was not
#' present in the cache. You can test if the returned value represents a
#' missing key by using the [is.key_missing()] function. You can
#' also have `get()` return a different sentinel value, like `NULL`.
#' If you want to throw an error on a cache miss, you can do so by providing a
#' function for `missing` that takes one argument, the key, and also use
#' `exec_missing=TRUE`.
#'
#' When the cache is created, you can supply a value for `missing`, which
#' sets the default value to be returned for missing values. It can also be
#' overridden when `get()` is called, by supplying a `missing`
#' argument. For example, if you use `cache$get("mykey", missing =
#' NULL)`, it will return `NULL` if the key is not in the cache.
#'
#' If your cache is configured so that `get()` returns a sentinel value
#' to represent a cache miss, then `set` will also not allow you to store
#' the sentinel value in the cache. It will throw an error if you attempt to
#' do so.
#'
#' Instead of returning the same sentinel value each time there is cache miss,
#' the cache can execute a function each time `get()` encounters missing
#' key. If the function returns a value, then `get()` will in turn return
#' that value. However, a more common use is for the function to throw an
#' error. If an error is thrown, then `get()` will not return a value.
#'
#' To do this, pass a one-argument function to `missing`, and use
#' `exec_missing=TRUE`. For example, if you want to throw an error that
#' prints the missing key, you could do this:
#'
#' \preformatted{
#' diskCache(
#' missing = function(key) {
#' stop("Attempted to get missing key: ", key)
#' },
#' exec_missing = TRUE
#' )
#' }
#'
#' If you use this, the code that calls `get()` should be wrapped with
#' [tryCatch()] to gracefully handle missing keys.
#'
#' @section Cache pruning:
#'
#' Cache pruning occurs when `set()` is called, or it can be invoked
#' manually by calling `prune()`.
#'
#' The disk cache will throttle the pruning so that it does not happen on
#' every call to `set()`, because the filesystem operations for checking
#' the status of files can be slow. Instead, it will prune once in every 20
#' calls to `set()`, or if at least 5 seconds have elapsed since the last
#' prune occurred, whichever is first. These parameters are currently not
#' customizable, but may be in the future.
#'
#' When a pruning occurs, if there are any objects that are older than
#' `max_age`, they will be removed.
#'
#' The `max_size` and `max_n` parameters are applied to the cache as
#' a whole, in contrast to `max_age`, which is applied to each object
#' individually.
#'
#' If the number of objects in the cache exceeds `max_n`, then objects
#' will be removed from the cache according to the eviction policy, which is
#' set with the `evict` parameter. Objects will be removed so that the
#' number of items is `max_n`.
#'
#' If the size of the objects in the cache exceeds `max_size`, then
#' objects will be removed from the cache. Objects will be removed from the
#' cache so that the total size remains under `max_size`. Note that the
#' size is calculated using the size of the files, not the size of disk space
#' used by the files --- these two values can differ because of files are
#' stored in blocks on disk. For example, if the block size is 4096 bytes,
#' then a file that is one byte in size will take 4096 bytes on disk.
#'
#' Another time that objects can be removed from the cache is when
#' `get()` is called. If the target object is older than `max_age`,
#' it will be removed and the cache will report it as a missing value.
#'
#' @section Eviction policies:
#'
#' If `max_n` or `max_size` are used, then objects will be removed
#' from the cache according to an eviction policy. The available eviction
#' policies are:
#'
#' \describe{
#' \item{`"lru"`}{
#' Least Recently Used. The least recently used objects will be removed.
#' This uses the filesystem's mtime property. When "lru" is used, each
#' `get()` is called, it will update the file's mtime.
#' }
#' \item{`"fifo"`}{
#' First-in-first-out. The oldest objects will be removed.
#' }
#' }
#'
#' Both of these policies use files' mtime. Note that some filesystems (notably
#' FAT) have poor mtime resolution. (atime is not used because support for
#' atime is worse than mtime.)
#'
#'
#' @section Sharing among multiple processes:
#'
#' The directory for a DiskCache can be shared among multiple R processes. To
#' do this, each R process should have a DiskCache object that uses the same
#' directory. Each DiskCache will do pruning independently of the others, so if
#' they have different pruning parameters, then one DiskCache may remove cached
#' objects before another DiskCache would do so.
#'
#' Even though it is possible for multiple processes to share a DiskCache
#' directory, this should not be done on networked file systems, because of
#' slow performance of networked file systems can cause problems. If you need
#' a high-performance shared cache, you can use one built on a database like
#' Redis, SQLite, mySQL, or similar.
#'
#' When multiple processes share a cache directory, there are some potential
#' race conditions. For example, if your code calls `exists(key)` to check
#' if an object is in the cache, and then call `get(key)`, the object may
#' be removed from the cache in between those two calls, and `get(key)`
#' will throw an error. Instead of calling the two functions, it is better to
#' simply call `get(key)`, and use `tryCatch()` to handle the error
#' that is thrown if the object is not in the cache. This effectively tests for
#' existence and gets the object in one operation.
#'
#' It is also possible for one processes to prune objects at the same time that
#' another processes is trying to prune objects. If this happens, you may see
#' a warning from `file.remove()` failing to remove a file that has
#' already been deleted.
#'
#'
#' @section Methods:
#'
#' A disk cache object has the following methods:
#'
#' \describe{
#' \item{`get(key, missing, exec_missing)`}{
#' Returns the value associated with `key`. If the key is not in the
#' cache, then it returns the value specified by `missing` or,
#' `missing` is a function and `exec_missing=TRUE`, then
#' executes `missing`. The function can throw an error or return the
#' value. If either of these parameters are specified here, then they
#' will override the defaults that were set when the DiskCache object was
#' created. See section Missing Keys for more information.
#' }
#' \item{`set(key, value)`}{
#' Stores the `key`-`value` pair in the cache.
#' }
#' \item{`exists(key)`}{
#' Returns `TRUE` if the cache contains the key, otherwise
#' `FALSE`.
#' }
#' \item{`size()`}{
#' Returns the number of items currently in the cache.
#' }
#' \item{`keys()`}{
#' Returns a character vector of all keys currently in the cache.
#' }
#' \item{`reset()`}{
#' Clears all objects from the cache.
#' }
#' \item{`destroy()`}{
#' Clears all objects in the cache, and removes the cache directory from
#' disk.
#' }
#' \item{`prune()`}{
#' Prunes the cache, using the parameters specified by `max_size`,
#' `max_age`, `max_n`, and `evict`.
#' }
#' }
#'
#' @param dir Directory to store files for the cache. If `NULL` (the
#' default) it will create and use a temporary directory.
#' @param max_age Maximum age of files in cache before they are evicted, in
#' seconds. Use `Inf` for no age limit.
#' @param 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
#' `evict`. Use `Inf` for no size limit.
#' @param max_n Maximum number of objects in the cache. If the number of objects
#' exceeds this value, then cached objects will be removed according to the
#' value of `evict`. Use `Inf` for no limit of number of items.
#' @param evict The eviction policy to use to decide which objects are removed
#' when a cache pruning occurs. Currently, `"lru"` and `"fifo"` are
#' supported.
#' @param destroy_on_finalize If `TRUE`, then when the DiskCache object is
#' garbage collected, the cache directory and all objects inside of it will be
#' deleted from disk. If `FALSE` (the default), it will do nothing when
#' finalized.
#' @param missing A value to return or a function to execute when
#' `get(key)` is called but the key is not present in the cache. The
#' default is a [key_missing()] object. If it is a function to
#' execute, the function must take one argument (the key), and you must also
#' use `exec_missing = TRUE`. If it is a function, it is useful in most
#' cases for it to throw an error, although another option is to return a
#' value. If a value is returned, that value will in turn be returned by
#' `get()`. See section Missing keys for more information.
#' @param exec_missing If `FALSE` (the default), then treat `missing`
#' as a value to return when `get()` results in a cache miss. If
#' `TRUE`, treat `missing` as a function to execute when
#' `get()` results in a cache miss.
#' @param logfile An optional filename or connection object to where logging
#' information will be written. To log to the console, use `stdout()`.
#'
#' @export
diskCache <- function(
dir = NULL,
max_size = 10 * 1024 ^ 2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
destroy_on_finalize = FALSE,
missing = key_missing(),
exec_missing = FALSE,
logfile = NULL)
{
DiskCache$new(dir, max_size, max_age, max_n, evict, destroy_on_finalize,
missing, exec_missing, logfile)
}
DiskCache <- R6Class("DiskCache",
public = list(
initialize = function(
dir = NULL,
max_size = 10 * 1024 ^ 2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
destroy_on_finalize = FALSE,
missing = key_missing(),
exec_missing = FALSE,
logfile = NULL)
{
if (exec_missing && (!is.function(missing) || length(formals(missing)) == 0)) {
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
}
if (is.null(dir)) {
dir <- tempfile("DiskCache-")
}
if (!is.numeric(max_size)) stop("max_size must be a number. Use `Inf` for no limit.")
if (!is.numeric(max_age)) stop("max_age must be a number. Use `Inf` for no limit.")
if (!is.numeric(max_n)) stop("max_n must be a number. Use `Inf` for no limit.")
if (!dirExists(dir)) {
private$log(paste0("initialize: Creating ", dir))
dir.create(dir, recursive = TRUE)
}
private$dir <- normalizePath(dir)
private$max_size <- max_size
private$max_age <- max_age
private$max_n <- max_n
private$evict <- match.arg(evict)
private$destroy_on_finalize <- destroy_on_finalize
private$missing <- missing
private$exec_missing <- exec_missing
private$logfile <- logfile
private$prune_last_time <- as.numeric(Sys.time())
},
get = function(key, missing = private$missing, exec_missing = private$exec_missing) {
private$log(paste0('get: key "', key, '"'))
self$is_destroyed(throw = TRUE)
validate_key(key)
private$maybe_prune_single(key)
filename <- private$key_to_filename(key)
# Instead of calling exists() before fetching the value, just try to
# fetch the value. This reduces the risk of a race condition when
# multiple processes share a cache.
read_error <- FALSE
tryCatch(
{
value <- suppressWarnings(readRDS(filename))
if (private$evict == "lru"){
Sys.setFileTime(filename, Sys.time())
}
},
error = function(e) {
read_error <<- TRUE
}
)
if (read_error) {
private$log(paste0('get: key "', key, '" is missing'))
if (exec_missing) {
if (!is.function(missing) || length(formals(missing)) == 0) {
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
}
return(missing(key))
} else {
return(missing)
}
}
private$log(paste0('get: key "', key, '" found'))
value
},
set = function(key, value) {
private$log(paste0('set: key "', key, '"'))
self$is_destroyed(throw = TRUE)
validate_key(key)
file <- private$key_to_filename(key)
temp_file <- paste0(file, "-temp-", createUniqueId(8))
save_error <- FALSE
ref_object <- FALSE
tryCatch(
{
saveRDS(value, file = temp_file,
refhook = function(x) {
ref_object <<- TRUE
NULL
}
)
file.rename(temp_file, file)
},
error = function(e) {
save_error <<- TRUE
# Unlike file.remove(), unlink() does not raise warning if file does
# not exist.
unlink(temp_file)
}
)
if (save_error) {
private$log(paste0('set: key "', key, '" error'))
stop('Error setting value for key "', key, '".')
}
if (ref_object) {
private$log(paste0('set: value is a reference object'))
warning("A reference object was cached in a serialized format. The restored object may not work as expected.")
}
private$prune_throttled()
invisible(self)
},
exists = function(key) {
self$is_destroyed(throw = TRUE)
validate_key(key)
file.exists(private$key_to_filename(key))
},
# Return all keys in the cache
keys = function() {
self$is_destroyed(throw = TRUE)
files <- dir(private$dir, "\\.rds$")
sub("\\.rds$", "", files)
},
remove = function(key) {
private$log(paste0('remove: key "', key, '"'))
self$is_destroyed(throw = TRUE)
validate_key(key)
file.remove(private$key_to_filename(key))
invisible(self)
},
reset = function() {
private$log(paste0('reset'))
self$is_destroyed(throw = TRUE)
file.remove(dir(private$dir, "\\.rds$", full.names = TRUE))
invisible(self)
},
prune = function() {
# TODO: It would be good to add parameters `n` and `size`, so that the
# cache can be pruned to `max_n - n` and `max_size - size` before adding
# an object. Right now we prune after adding the object, so the cache
# can temporarily grow past the limits. The reason we don't do this now
# is because it is expensive to find the size of the serialized object
# before adding it.
private$log(paste0('prune'))
self$is_destroyed(throw = TRUE)
current_time <- Sys.time()
filenames <- dir(private$dir, "\\.rds$", full.names = TRUE)
info <- file.info(filenames)
info <- info[info$isdir == FALSE, ]
info$name <- rownames(info)
rownames(info) <- NULL
# Files could be removed between the dir() and file.info() calls. The
# entire row for such files will have NA values. Remove those rows.
info <- info[!is.na(info$size), ]
# 1. Remove any files where the age exceeds max age.
if (is.finite(private$max_age)) {
timediff <- as.numeric(current_time - info$mtime, units = "secs")
rm_idx <- timediff > private$max_age
if (any(rm_idx)) {
private$log(paste0("prune max_age: Removing ", paste(info$name[rm_idx], collapse = ", ")))
file.remove(info$name[rm_idx])
info <- info[!rm_idx, ]
}
}
# Sort objects by priority. The sorting is done in a function which can be
# called multiple times but only does the work the first time.
info_is_sorted <- FALSE
ensure_info_is_sorted <- function() {
if (info_is_sorted) return()
info <<- info[order(info$mtime, decreasing = TRUE), ]
info_is_sorted <<- TRUE
}
# 2. Remove files if there are too many.
if (is.finite(private$max_n) && nrow(info) > private$max_n) {
ensure_info_is_sorted()
rm_idx <- seq_len(nrow(info)) > private$max_n
private$log(paste0("prune max_n: Removing ", paste(info$name[rm_idx], collapse = ", ")))
rm_success <- file.remove(info$name[rm_idx])
info <- info[!rm_success, ]
}
# 3. Remove files if cache is too large.
if (is.finite(private$max_size) && sum(info$size) > private$max_size) {
ensure_info_is_sorted()
cum_size <- cumsum(info$size)
rm_idx <- cum_size > private$max_size
private$log(paste0("prune max_size: Removing ", paste(info$name[rm_idx], collapse = ", ")))
rm_success <- file.remove(info$name[rm_idx])
info <- info[!rm_success, ]
}
private$prune_last_time <- as.numeric(current_time)
invisible(self)
},
size = function() {
self$is_destroyed(throw = TRUE)
length(dir(private$dir, "\\.rds$"))
},
destroy = function() {
if (self$is_destroyed()) {
return(invisible(self))
}
private$log(paste0("destroy: Removing ", private$dir))
# First create a sentinel file so that other processes sharing this
# cache know that the cache is to be destroyed. This is needed because
# the recursive unlink is not atomic: another process can add a file to
# the directory after unlink starts removing files but before it removes
# the directory, and when that happens, the directory removal will fail.
file.create(file.path(private$dir, "__destroyed__"))
# Remove all the .rds files. This will not remove the setinel file.
file.remove(dir(private$dir, "\\.rds$", full.names = TRUE))
# Next remove dir recursively, including sentinel file.
unlink(private$dir, recursive = TRUE)
private$destroyed <- TRUE
invisible(self)
},
is_destroyed = function(throw = FALSE) {
if (!dirExists(private$dir) ||
file.exists(file.path(private$dir, "__destroyed__")))
{
# It's possible for another process to destroy a shared cache directory
private$destroyed <- TRUE
}
if (throw) {
if (private$destroyed) {
stop("Attempted to use cache which has been destroyed:\n ", private$dir)
}
} else {
private$destroyed
}
},
finalize = function() {
if (private$destroy_on_finalize) {
self$destroy()
}
}
),
private = list(
dir = NULL,
max_age = NULL,
max_size = NULL,
max_n = NULL,
evict = NULL,
destroy_on_finalize = NULL,
destroyed = FALSE,
missing = NULL,
exec_missing = FALSE,
logfile = NULL,
prune_throttle_counter = 0,
prune_last_time = NULL,
key_to_filename = function(key) {
validate_key(key)
# Additional validation. This 80-char limit is arbitrary, and is
# intended to avoid hitting a filename length limit on Windows.
if (nchar(key) > 80) {
stop("Invalid key: key must have fewer than 80 characters.")
}
file.path(private$dir, paste0(key, ".rds"))
},
# A wrapper for prune() that throttles it, because prune() can be
# expensive due to filesystem operations. This function will prune only
# once every 20 times it is called, or if it has been more than 5 seconds
# since the last time the cache was actually pruned, whichever is first.
# In the future, the behavior may be customizable.
prune_throttled = function() {
# Count the number of times prune() has been called.
private$prune_throttle_counter <- private$prune_throttle_counter + 1
if (private$prune_throttle_counter > 20 ||
private$prune_last_time - as.numeric(Sys.time()) > 5)
{
self$prune()
private$prune_throttle_counter <- 0
}
},
# Prunes a single object if it exceeds max_age. If the object does not
# exceed max_age, or if the object doesn't exist, do nothing.
maybe_prune_single = function(key) {
obj <- private$cache[[key]]
if (is.null(obj)) return()
timediff <- as.numeric(Sys.time()) - obj$mtime
if (timediff > private$max_age) {
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
rm(list = key, envir = private$cache)
}
},
log = function(text) {
if (is.null(private$logfile)) return()
text <- paste0(format(Sys.time(), "[%Y-%m-%d %H:%M:%OS3] DiskCache "), text)
writeLines(text, private$logfile)
}
)
)

View File

@@ -1,365 +0,0 @@
#' Create a memory cache object
#'
#' A memory cache object is a key-value store that saves the values in an
#' environment. Objects can be stored and retrieved using the `get()` and
#' `set()` methods. Objects are automatically pruned from the cache
#' according to the parameters `max_size`, `max_age`, `max_n`,
#' and `evict`.
#'
#' In a `MemoryCache`, R objects are stored directly in the cache; they are
#' not *not* serialized before being stored in the cache. This contrasts
#' with other cache types, like [diskCache()], where objects are
#' serialized, and the serialized object is cached. This can result in some
#' differences of behavior. For example, as long as an object is stored in a
#' MemoryCache, it will not be garbage collected.
#'
#'
#' @section Missing keys:
#' The `missing` and `exec_missing` parameters controls what happens
#' when `get()` is called with a key that is not in the cache (a cache
#' miss). The default behavior is to return a [key_missing()]
#' object. This is a *sentinel value* that indicates that the key was not
#' present in the cache. You can test if the returned value represents a
#' missing key by using the [is.key_missing()] function. You can
#' also have `get()` return a different sentinel value, like `NULL`.
#' If you want to throw an error on a cache miss, you can do so by providing a
#' function for `missing` that takes one argument, the key, and also use
#' `exec_missing=TRUE`.
#'
#' When the cache is created, you can supply a value for `missing`, which
#' sets the default value to be returned for missing values. It can also be
#' overridden when `get()` is called, by supplying a `missing`
#' argument. For example, if you use `cache$get("mykey", missing =
#' NULL)`, it will return `NULL` if the key is not in the cache.
#'
#' If your cache is configured so that `get()` returns a sentinel value
#' to represent a cache miss, then `set` will also not allow you to store
#' the sentinel value in the cache. It will throw an error if you attempt to
#' do so.
#'
#' Instead of returning the same sentinel value each time there is cache miss,
#' the cache can execute a function each time `get()` encounters missing
#' key. If the function returns a value, then `get()` will in turn return
#' that value. However, a more common use is for the function to throw an
#' error. If an error is thrown, then `get()` will not return a value.
#'
#' To do this, pass a one-argument function to `missing`, and use
#' `exec_missing=TRUE`. For example, if you want to throw an error that
#' prints the missing key, you could do this:
#'
#' \preformatted{
#' diskCache(
#' missing = function(key) {
#' stop("Attempted to get missing key: ", key)
#' },
#' exec_missing = TRUE
#' )
#' }
#'
#' If you use this, the code that calls `get()` should be wrapped with
#' [tryCatch()] to gracefully handle missing keys.
#'
#' @section Cache pruning:
#'
#' Cache pruning occurs when `set()` is called, or it can be invoked
#' manually by calling `prune()`.
#'
#' When a pruning occurs, if there are any objects that are older than
#' `max_age`, they will be removed.
#'
#' The `max_size` and `max_n` parameters are applied to the cache as
#' a whole, in contrast to `max_age`, which is applied to each object
#' individually.
#'
#' If the number of objects in the cache exceeds `max_n`, then objects
#' will be removed from the cache according to the eviction policy, which is
#' set with the `evict` parameter. Objects will be removed so that the
#' number of items is `max_n`.
#'
#' If the size of the objects in the cache exceeds `max_size`, then
#' objects will be removed from the cache. Objects will be removed from the
#' cache so that the total size remains under `max_size`. Note that the
#' size is calculated using the size of the files, not the size of disk space
#' used by the files --- these two values can differ because of files are
#' stored in blocks on disk. For example, if the block size is 4096 bytes,
#' then a file that is one byte in size will take 4096 bytes on disk.
#'
#' Another time that objects can be removed from the cache is when
#' `get()` is called. If the target object is older than `max_age`,
#' it will be removed and the cache will report it as a missing value.
#'
#' @section Eviction policies:
#'
#' If `max_n` or `max_size` are used, then objects will be removed
#' from the cache according to an eviction policy. The available eviction
#' policies are:
#'
#' \describe{
#' \item{`"lru"`}{
#' Least Recently Used. The least recently used objects will be removed.
#' This uses the filesystem's atime property. Some filesystems do not
#' support atime, or have a very low atime resolution. The DiskCache will
#' check for atime support, and if the filesystem does not support atime,
#' a warning will be issued and the "fifo" policy will be used instead.
#' }
#' \item{`"fifo"`}{
#' First-in-first-out. The oldest objects will be removed.
#' }
#' }
#'
#' @section Methods:
#'
#' A disk cache object has the following methods:
#'
#' \describe{
#' \item{`get(key, missing, exec_missing)`}{
#' Returns the value associated with `key`. If the key is not in the
#' cache, then it returns the value specified by `missing` or,
#' `missing` is a function and `exec_missing=TRUE`, then
#' executes `missing`. The function can throw an error or return the
#' value. If either of these parameters are specified here, then they
#' will override the defaults that were set when the DiskCache object was
#' created. See section Missing Keys for more information.
#' }
#' \item{`set(key, value)`}{
#' Stores the `key`-`value` pair in the cache.
#' }
#' \item{`exists(key)`}{
#' Returns `TRUE` if the cache contains the key, otherwise
#' `FALSE`.
#' }
#' \item{`size()`}{
#' Returns the number of items currently in the cache.
#' }
#' \item{`keys()`}{
#' Returns a character vector of all keys currently in the cache.
#' }
#' \item{`reset()`}{
#' Clears all objects from the cache.
#' }
#' \item{`destroy()`}{
#' Clears all objects in the cache, and removes the cache directory from
#' disk.
#' }
#' \item{`prune()`}{
#' Prunes the cache, using the parameters specified by `max_size`,
#' `max_age`, `max_n`, and `evict`.
#' }
#' }
#'
#' @inheritParams diskCache
#'
#' @export
memoryCache <- function(
max_size = 10 * 1024 ^ 2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
missing = key_missing(),
exec_missing = FALSE,
logfile = NULL)
{
MemoryCache$new(max_size, max_age, max_n, evict, missing, exec_missing, logfile)
}
MemoryCache <- R6Class("MemoryCache",
public = list(
initialize = function(
max_size = 10 * 1024 ^ 2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
missing = key_missing(),
exec_missing = FALSE,
logfile = NULL)
{
if (exec_missing && (!is.function(missing) || length(formals(missing)) == 0)) {
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
}
if (!is.numeric(max_size)) stop("max_size must be a number. Use `Inf` for no limit.")
if (!is.numeric(max_age)) stop("max_age must be a number. Use `Inf` for no limit.")
if (!is.numeric(max_n)) stop("max_n must be a number. Use `Inf` for no limit.")
private$cache <- fastmap()
private$max_size <- max_size
private$max_age <- max_age
private$max_n <- max_n
private$evict <- match.arg(evict)
private$missing <- missing
private$exec_missing <- exec_missing
private$logfile <- logfile
},
get = function(key, missing = private$missing, exec_missing = private$exec_missing) {
private$log(paste0('get: key "', key, '"'))
validate_key(key)
private$maybe_prune_single(key)
if (!self$exists(key)) {
private$log(paste0('get: key "', key, '" is missing'))
if (exec_missing) {
if (!is.function(missing) || length(formals(missing)) == 0) {
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
}
return(missing(key))
} else {
return(missing)
}
}
private$log(paste0('get: key "', key, '" found'))
value <- private$cache$get(key)$value
value
},
set = function(key, value) {
private$log(paste0('set: key "', key, '"'))
validate_key(key)
time <- as.numeric(Sys.time())
# Only record size if we're actually using max_size for pruning.
if (is.finite(private$max_size)) {
# Reported size is rough! See ?object.size.
size <- as.numeric(object.size(value))
} else {
size <- NULL
}
private$cache$set(key, list(
key = key,
value = value,
size = size,
mtime = time,
atime = time
))
self$prune()
invisible(self)
},
exists = function(key) {
validate_key(key)
private$cache$has(key)
},
keys = function() {
private$cache$keys()
},
remove = function(key) {
private$log(paste0('remove: key "', key, '"'))
validate_key(key)
private$cache$remove(key)
invisible(self)
},
reset = function() {
private$log(paste0('reset'))
private$cache$reset()
invisible(self)
},
prune = function() {
private$log(paste0('prune'))
info <- private$object_info()
# 1. Remove any objects where the age exceeds max age.
if (is.finite(private$max_age)) {
time <- as.numeric(Sys.time())
timediff <- time - info$mtime
rm_idx <- timediff > private$max_age
if (any(rm_idx)) {
private$log(paste0("prune max_age: Removing ", paste(info$key[rm_idx], collapse = ", ")))
private$cache$remove(info$key[rm_idx])
info <- info[!rm_idx, ]
}
}
# Sort objects by priority, according to eviction policy. The sorting is
# done in a function which can be called multiple times but only does
# the work the first time.
info_is_sorted <- FALSE
ensure_info_is_sorted <- function() {
if (info_is_sorted) return()
if (private$evict == "lru") {
info <<- info[order(info$atime, decreasing = TRUE), ]
} else if (private$evict == "fifo") {
info <<- info[order(info$mtime, decreasing = TRUE), ]
} else {
stop('Unknown eviction policy "', private$evict, '"')
}
info_is_sorted <<- TRUE
}
# 2. Remove objects if there are too many.
if (is.finite(private$max_n) && nrow(info) > private$max_n) {
ensure_info_is_sorted()
rm_idx <- seq_len(nrow(info)) > private$max_n
private$log(paste0("prune max_n: Removing ", paste(info$key[rm_idx], collapse = ", ")))
private$cache$remove(info$key[rm_idx])
info <- info[!rm_idx, ]
}
# 3. Remove objects if cache is too large.
if (is.finite(private$max_size) && sum(info$size) > private$max_size) {
ensure_info_is_sorted()
cum_size <- cumsum(info$size)
rm_idx <- cum_size > private$max_size
private$log(paste0("prune max_size: Removing ", paste(info$key[rm_idx], collapse = ", ")))
private$cache$remove(info$key[rm_idx])
info <- info[!rm_idx, ]
}
invisible(self)
},
size = function() {
length(self$keys())
}
),
private = list(
cache = NULL,
max_age = NULL,
max_size = NULL,
max_n = NULL,
evict = NULL,
missing = NULL,
exec_missing = NULL,
logfile = NULL,
# Prunes a single object if it exceeds max_age. If the object does not
# exceed max_age, or if the object doesn't exist, do nothing.
maybe_prune_single = function(key) {
if (!is.finite(private$max_age)) return()
obj <- private$cache$get(key)
if (is.null(obj)) return()
timediff <- as.numeric(Sys.time()) - obj$mtime
if (timediff > private$max_age) {
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
private$cache$remove(key)
}
},
object_info = function() {
keys <- private$cache$keys()
data.frame(
key = keys,
size = vapply(keys, function(key) private$cache$get(key)$size, 0),
mtime = vapply(keys, function(key) private$cache$get(key)$mtime, 0),
atime = vapply(keys, function(key) private$cache$get(key)$atime, 0),
stringsAsFactors = FALSE
)
},
log = function(text) {
if (is.null(private$logfile)) return()
text <- paste0(format(Sys.time(), "[%Y-%m-%d %H:%M:%OS3] MemoryCache "), text)
writeLines(text, private$logfile)
}
)
)

View File

@@ -1,9 +1,25 @@
validate_key <- function(key) {
if (!is.character(key) || length(key) != 1 || nchar(key) == 0) {
stop("Invalid key: key must be single non-empty string.")
}
if (grepl("[^a-z0-9]", key)) {
stop("Invalid key: ", key, ". Only lowercase letters and numbers are allowed.")
}
# For our purposes, cache objects must support these methods.
is_cache_object <- function(x) {
# Use tryCatch in case the object does not support `$`.
tryCatch(
is.function(x$get) && is.function(x$set),
error = function(e) FALSE
)
}
# Given a cache object, or string "app" or "session", return appropriate cache
# object.
resolve_cache_object <- function(cache, session) {
if (identical(cache, "app")) {
cache <- getShinyOption("cache", default = NULL)
} else if (identical(cache, "session")) {
cache <- session$cache
}
if (is_cache_object(cache)) {
return(cache)
}
stop('`cache` must either be "app", "session", or a cache object with methods, `$get`, and `$set`.')
}

View File

@@ -75,6 +75,18 @@ getCallNames <- function(calls) {
})
}
# A stripped down version of getCallNames() that intentionally avoids deparsing expressions.
# Instead, it leaves expressions to be directly `rlang::hash()` (for de-duplication), which
# is much faster than deparsing then hashing.
getCallNamesForHash <- function(calls) {
lapply(calls, function(call) {
name <- call[[1L]]
if (is.function(name)) return("<Anonymous>")
if (typeof(name) == "promise") return("<Promise>")
name
})
}
getLocs <- function(calls) {
vapply(calls, function(call) {
srcref <- attr(call, "srcref", exact = TRUE)
@@ -122,7 +134,9 @@ getCallCategories <- function(calls) {
#' @rdname stacktrace
#' @export
captureStackTraces <- function(expr) {
promises::with_promise_domain(createStackTracePromiseDomain(),
# Use `promises::` as it shows up in the stack trace
promises::with_promise_domain(
createStackTracePromiseDomain(),
expr
)
}
@@ -130,11 +144,49 @@ captureStackTraces <- function(expr) {
#' @include globals.R
.globals$deepStack <- NULL
getCallStackDigest <- function(callStack, warn = FALSE) {
dg <- attr(callStack, "shiny.stack.digest", exact = TRUE)
if (!is.null(dg)) {
return(dg)
}
if (isTRUE(warn)) {
rlang::warn(
"Call stack doesn't have a cached digest; expensively computing one now",
.frequency = "once",
.frequency_id = "deepstack-uncached-digest-warning"
)
}
rlang::hash(getCallNamesForHash(callStack))
}
saveCallStackDigest <- function(callStack) {
attr(callStack, "shiny.stack.digest") <- getCallStackDigest(callStack, warn = FALSE)
callStack
}
# Appends a call stack to a list of call stacks, but only if it's not already
# in the list. The list is deduplicated by digest; ideally the digests on the
# list are cached before calling this function (you will get a warning if not).
appendCallStackWithDedupe <- function(lst, x) {
digests <- vapply(lst, getCallStackDigest, character(1), warn = TRUE)
xdigest <- getCallStackDigest(x, warn = TRUE)
stopifnot(all(nzchar(digests)))
stopifnot(length(xdigest) == 1)
stopifnot(nzchar(xdigest))
if (xdigest %in% digests) {
return(lst)
} else {
return(c(lst, list(x)))
}
}
createStackTracePromiseDomain <- function() {
# These are actually stateless, we wouldn't have to create a new one each time
# if we didn't want to. They're pretty cheap though.
d <- promises::new_promise_domain(
d <- new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
# Subscription time
@@ -142,13 +194,14 @@ createStackTracePromiseDomain <- function() {
currentStack <- sys.calls()
currentParents <- sys.parents()
attr(currentStack, "parents") <- currentParents
currentStack <- saveCallStackDigest(currentStack)
currentDeepStack <- .globals$deepStack
}
function(...) {
# Fulfill time
if (deepStacksEnabled()) {
origDeepStack <- .globals$deepStack
.globals$deepStack <- c(currentDeepStack, list(currentStack))
.globals$deepStack <- appendCallStackWithDedupe(currentDeepStack, currentStack)
on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
}
@@ -165,13 +218,14 @@ createStackTracePromiseDomain <- function() {
currentStack <- sys.calls()
currentParents <- sys.parents()
attr(currentStack, "parents") <- currentParents
currentStack <- saveCallStackDigest(currentStack)
currentDeepStack <- .globals$deepStack
}
function(...) {
# Fulfill time
if (deepStacksEnabled()) {
origDeepStack <- .globals$deepStack
.globals$deepStack <- c(currentDeepStack, list(currentStack))
.globals$deepStack <- appendCallStackWithDedupe(currentDeepStack, currentStack)
on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
}
@@ -199,6 +253,7 @@ doCaptureStack <- function(e) {
calls <- sys.calls()
parents <- sys.parents()
attr(calls, "parents") <- parents
calls <- saveCallStackDigest(calls)
attr(e, "stack.trace") <- calls
}
if (deepStacksEnabled()) {
@@ -217,7 +272,7 @@ doCaptureStack <- function(e) {
#' @rdname stacktrace
#' @export
withLogErrors <- function(expr,
full = getOption("shiny.fullstacktrace", FALSE),
full = get_devmode_option("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
withCallingHandlers(
@@ -225,10 +280,12 @@ withLogErrors <- function(expr,
result <- captureStackTraces(expr)
# Handle expr being an async operation
if (promises::is.promise(result)) {
if (is.promise(result)) {
result <- promises::catch(result, function(cond) {
# Don't print shiny.silent.error (i.e. validation errors)
if (inherits(cond, "shiny.silent.error")) return()
if (cnd_inherits(cond, "shiny.silent.error")) {
return()
}
if (isTRUE(getOption("show.error.messages"))) {
printError(cond, full = full, offset = offset)
}
@@ -239,7 +296,7 @@ withLogErrors <- function(expr,
},
error = function(cond) {
# Don't print shiny.silent.error (i.e. validation errors)
if (inherits(cond, "shiny.silent.error")) return()
if (cnd_inherits(cond, "shiny.silent.error")) return()
if (isTRUE(getOption("show.error.messages"))) {
printError(cond, full = full, offset = offset)
}
@@ -264,176 +321,128 @@ withLogErrors <- function(expr,
#' @rdname stacktrace
#' @export
printError <- function(cond,
full = getOption("shiny.fullstacktrace", FALSE),
full = get_devmode_option("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
warning(call. = FALSE, immediate. = TRUE, sprintf("Error in %s: %s",
warning(call. = FALSE, immediate. = TRUE, sprintf("Error in %s: %s",
getCallNames(list(conditionCall(cond))), conditionMessage(cond)))
printStackTrace(cond, full = full, offset = offset)
}
#' @rdname stacktrace
#' @export
printStackTrace <- function(cond,
full = getOption("shiny.fullstacktrace", FALSE),
full = get_devmode_option("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
stackTraces <- c(
attr(cond, "deep.stack.trace", exact = TRUE),
list(attr(cond, "stack.trace", exact = TRUE))
)
# Stripping of stack traces is the one step where the different stack traces
# interact. So we need to do this in one go, instead of individually within
# printOneStackTrace.
if (!full) {
stripResults <- stripStackTraces(lapply(stackTraces, getCallNames))
} else {
# If full is TRUE, we don't want to strip anything
stripResults <- rep_len(list(TRUE), length(stackTraces))
}
mapply(
seq_along(stackTraces),
rev(stackTraces),
rev(stripResults),
FUN = function(i, trace, stripResult) {
if (is.integer(trace)) {
noun <- if (trace > 1L) "traces" else "trace"
message("[ reached getOption(\"shiny.deepstacktrace\") -- omitted ", trace, " more stack ", noun, " ]")
} else {
if (i != 1) {
message("From earlier call:")
}
printOneStackTrace(
stackTrace = trace,
stripResult = stripResult,
full = full,
offset = offset
)
}
# No mapply return value--we're just printing
NULL
},
SIMPLIFY = FALSE
)
invisible()
}
printOneStackTrace <- function(stackTrace, stripResult, full, offset) {
calls <- offsetSrcrefs(stackTrace, offset = offset)
callNames <- getCallNames(stackTrace)
parents <- attr(stackTrace, "parents", exact = TRUE)
should_drop <- !full
should_strip <- !full
should_prune <- !full
stackTraceCalls <- c(
attr(cond, "deep.stack.trace", exact = TRUE),
list(attr(cond, "stack.trace", exact = TRUE))
)
stackTraceParents <- lapply(stackTraceCalls, attr, which = "parents", exact = TRUE)
stackTraceCallNames <- lapply(stackTraceCalls, getCallNames)
stackTraceCalls <- lapply(stackTraceCalls, offsetSrcrefs, offset = offset)
# Use dropTrivialFrames logic to remove trailing bits (.handleSimpleError, h)
if (should_drop) {
# toKeep is a list of logical vectors, of which elements (stack frames) to keep
toKeep <- lapply(stackTraceCallNames, dropTrivialFrames)
# We apply the list of logical vector indices to each data structure
stackTraceCalls <- mapply(stackTraceCalls, FUN = `[`, toKeep, SIMPLIFY = FALSE)
stackTraceCallNames <- mapply(stackTraceCallNames, FUN = `[`, toKeep, SIMPLIFY = FALSE)
stackTraceParents <- mapply(stackTraceParents, FUN = `[`, toKeep, SIMPLIFY = FALSE)
toKeep <- dropTrivialFrames(callNames)
calls <- calls[toKeep]
callNames <- callNames[toKeep]
parents <- parents[toKeep]
stripResult <- stripResult[toKeep]
}
delayedAssign("all_true", {
# List of logical vectors that are all TRUE, the same shape as
# stackTraceCallNames. Delay the evaluation so we don't create it unless
# we need it, but if we need it twice then we don't pay to create it twice.
lapply(stackTraceCallNames, function(st) {
rep_len(TRUE, length(st))
})
})
# stripStackTraces and lapply(stackTraceParents, pruneStackTrace) return lists
# of logical vectors. Use mapply(FUN = `&`) to boolean-and each pair of the
# logical vectors.
toShow <- mapply(
if (should_strip) stripStackTraces(stackTraceCallNames) else all_true,
if (should_prune) lapply(stackTraceParents, pruneStackTrace) else all_true,
FUN = `&`,
SIMPLIFY = FALSE
)
dfs <- mapply(seq_along(stackTraceCalls), rev(stackTraceCalls), rev(stackTraceCallNames), rev(toShow), FUN = function(i, calls, nms, index) {
st <- data.frame(
num = rev(which(index)),
call = rev(nms[index]),
loc = rev(getLocs(calls[index])),
category = rev(getCallCategories(calls[index])),
stringsAsFactors = FALSE
)
if (i != 1) {
message("From earlier call:")
}
if (nrow(st) == 0) {
message(" [No stack trace available]")
} else {
width <- floor(log10(max(st$num))) + 1
formatted <- paste0(
" ",
formatC(st$num, width = width),
": ",
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
if (category == "pkg")
crayon::silver(name)
else if (category == "user")
crayon::blue$bold(name)
else
crayon::white(name)
}),
"\n"
)
cat(file = stderr(), formatted, sep = "")
}
st
}, SIMPLIFY = FALSE)
invisible()
}
#' @details `extractStackTrace` takes a list of calls (e.g. as returned
#' from `conditionStackTrace(cond)`) and returns a data frame with one
#' row for each stack frame and the columns `num` (stack frame number),
#' `call` (a function name or similar), and `loc` (source file path
#' and line number, if available). It was deprecated after shiny 1.0.5 because
#' it doesn't support deep stack traces.
#' @rdname stacktrace
#' @export
extractStackTrace <- function(calls,
full = getOption("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
shinyDeprecated(NULL,
"extractStackTrace is deprecated. Please contact the Shiny team if you were using this functionality.",
version = "1.0.5")
srcrefs <- getSrcRefs(calls)
if (offset) {
# Offset calls vs. srcrefs by 1 to make them more intuitive.
# E.g. for "foo [bar.R:10]", line 10 of bar.R will be part of
# the definition of foo().
srcrefs <- c(utils::tail(srcrefs, -1), list(NULL))
toShow <- rep(TRUE, length(callNames))
if (should_prune) {
toShow <- toShow & pruneStackTrace(parents)
}
calls <- setSrcRefs(calls, srcrefs)
callnames <- getCallNames(calls)
# Hide and show parts of the callstack based on ..stacktrace(on|off)..
if (full) {
toShow <- rep.int(TRUE, length(calls))
} else {
# Remove stop(), .handleSimpleError(), and h() calls from the end of
# the calls--they don't add any helpful information. But only remove
# the last *contiguous* block of them, and then, only if they are the
# last thing in the calls list.
hideable <- callnames %in% c("stop", ".handleSimpleError", "h")
# What's the last that *didn't* match stop/.handleSimpleError/h?
lastGoodCall <- max(which(!hideable))
toRemove <- length(calls) - lastGoodCall
# But don't remove more than 5 levels--that's an indication we might
# have gotten it wrong, I guess
if (toRemove > 0 && toRemove < 5) {
calls <- utils::head(calls, -toRemove)
callnames <- utils::head(callnames, -toRemove)
}
# This uses a ref-counting scheme. It might make sense to switch this
# to a toggling scheme, so the most recent ..stacktrace(on|off)..
# directive wins, regardless of what came before it.
# Also explicitly remove ..stacktraceon.. because it can appear with
# score > 0 but still should never be shown.
score <- rep.int(0, length(callnames))
score[callnames == "..stacktraceoff.."] <- -1
score[callnames == "..stacktraceon.."] <- 1
toShow <- (1 + cumsum(score)) > 0 & !(callnames %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
# doTryCatch, tryCatchOne, and tryCatchList are not informative--they're
# just internals for tryCatch
toShow <- toShow & !(callnames %in% c("doTryCatch", "tryCatchOne", "tryCatchList"))
if (should_strip) {
toShow <- toShow & stripResult
}
calls <- calls[toShow]
calls <- rev(calls) # Show in traceback() order
index <- rev(which(toShow))
width <- floor(log10(max(index))) + 1
# If we're running in testthat, hide the parts of the stack trace that can
# vary based on how testthat was launched. It's critical that this is not
# happen at the same time as dropTrivialFrames, which happens before
# pruneStackTrace; because dropTrivialTestFrames removes calls from the top
# (or bottom? whichever is the oldest?) of the stack, it breaks `parents`
# which is based on absolute indices of calls. dropTrivialFrames gets away
# with this because it only removes calls from the opposite side of the stack.
toShow <- toShow & dropTrivialTestFrames(callNames)
data.frame(
num = index,
call = getCallNames(calls),
loc = getLocs(calls),
category = getCallCategories(calls),
st <- data.frame(
num = rev(which(toShow)),
call = rev(callNames[toShow]),
loc = rev(getLocs(calls[toShow])),
category = rev(getCallCategories(calls[toShow])),
stringsAsFactors = FALSE
)
if (nrow(st) == 0) {
message(" [No stack trace available]")
} else {
width <- floor(log10(max(st$num))) + 1
formatted <- paste0(
" ",
formatC(st$num, width = width),
": ",
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
if (category == "pkg")
cli::col_silver(name)
else if (category == "user")
cli::style_bold(cli::col_blue(name))
else
cli::col_white(name)
}),
"\n"
)
cat(file = stderr(), formatted, sep = "")
}
invisible(st)
}
stripStackTraces <- function(stackTraces, values = FALSE) {
@@ -459,19 +468,19 @@ stripOneStackTrace <- function(stackTrace, truncateFloor, startingScore) {
prefix <- rep_len(FALSE, indexOfFloor)
}
}
if (length(stackTrace) == 0) {
return(list(score = startingScore, character(0)))
}
score <- rep.int(0L, length(stackTrace))
score[stackTrace == "..stacktraceon.."] <- 1L
score[stackTrace == "..stacktraceoff.."] <- -1L
score <- startingScore + cumsum(score)
toShow <- score > 0 & !(stackTrace %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
list(score = utils::tail(score, 1), trace = c(prefix, toShow))
}
@@ -486,23 +495,32 @@ pruneStackTrace <- function(parents) {
# sufficient; we also need to drop nodes that are the last child, but one of
# their ancestors is not.
is_dupe <- duplicated(parents, fromLast = TRUE)
# The index of the most recently seen node that was actually kept instead of
# dropped.
current_node <- 0
# Loop over the parent indices. Anything that is not parented by current_node
# (a.k.a. last-known-good node), or is a dupe, can be discarded. Anything that
# is kept becomes the new current_node.
#
# jcheng 2022-03-18: Two more reasons a node can be kept:
# 1. parent is 0
# 2. parent is i
# Not sure why either of these situations happen, but they're common when
# interacting with rlang/dplyr errors. See issue rstudio/shiny#3250 for repro
# cases.
include <- vapply(seq_along(parents), function(i) {
if (!is_dupe[[i]] && parents[[i]] == current_node) {
if ((!is_dupe[[i]] && parents[[i]] == current_node) ||
parents[[i]] == 0 ||
parents[[i]] == i) {
current_node <<- i
TRUE
} else {
FALSE
}
}, FUN.VALUE = logical(1))
include
}
@@ -515,13 +533,41 @@ dropTrivialFrames <- function(callnames) {
# What's the last that *didn't* match stop/.handleSimpleError/h?
lastGoodCall <- max(which(!hideable))
toRemove <- length(callnames) - lastGoodCall
c(
rep_len(TRUE, length(callnames) - toRemove),
rep_len(FALSE, toRemove)
)
}
dropTrivialTestFrames <- function(callnames) {
if (!identical(Sys.getenv("TESTTHAT_IS_SNAPSHOT"), "true")) {
return(rep_len(TRUE, length(callnames)))
}
hideable <- callnames %in% c(
"test",
"devtools::test",
"test_check",
"testthat::test_check",
"test_dir",
"testthat::test_dir",
"test_file",
"testthat::test_file",
"test_local",
"testthat::test_local"
)
# Remove everything from inception to calling the test
# It shouldn't matter how you get there, just that you're finally testing
toRemove <- max(which(hideable))
c(
rep_len(FALSE, toRemove),
rep_len(TRUE, length(callnames) - toRemove)
)
}
offsetSrcrefs <- function(calls, offset = TRUE) {
if (offset) {
srcrefs <- getSrcRefs(calls)
@@ -530,48 +576,12 @@ offsetSrcrefs <- function(calls, offset = TRUE) {
# E.g. for "foo [bar.R:10]", line 10 of bar.R will be part of
# the definition of foo().
srcrefs <- c(utils::tail(srcrefs, -1), list(NULL))
calls <- setSrcRefs(calls, srcrefs)
}
calls
}
#' @details `formatStackTrace` is similar to `extractStackTrace`, but
#' it returns a preformatted character vector instead of a data frame. It was
#' deprecated after shiny 1.0.5 because it doesn't support deep stack traces.
#' @param indent A string to prefix every line of the stack trace.
#' @rdname stacktrace
#' @export
formatStackTrace <- function(calls, indent = " ",
full = getOption("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
shinyDeprecated(NULL,
"extractStackTrace is deprecated. Please contact the Shiny team if you were using this functionality.",
version = "1.0.5")
st <- extractStackTrace(calls, full = full, offset = offset)
if (nrow(st) == 0) {
return(character(0))
}
width <- floor(log10(max(st$num))) + 1
paste0(
indent,
formatC(st$num, width = width),
": ",
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
if (category == "pkg")
crayon::silver(name)
else if (category == "user")
crayon::blue$bold(name)
else
crayon::white(name)
})
)
}
getSrcRefs <- function(calls) {
lapply(calls, function(call) {
attr(call, "srcref", exact = TRUE)

121
R/deprecated.R Normal file
View File

@@ -0,0 +1,121 @@
#' Print message for deprecated functions in Shiny
#'
#' To disable these messages, use `options(shiny.deprecation.messages=FALSE)`.
#'
#' @param version Shiny version when the function was deprecated
#' @param what Function with possible arguments
#' @param with Possible function with arguments that should be used instead
#' @param details Additional information to be added after a new line to the displayed message
#' @keywords internal
shinyDeprecated <- function(
version,
what,
with = NULL,
details = NULL,
type = c("deprecated", "superseded")
) {
if (is_false(getOption("shiny.deprecation.messages"))) {
return(invisible())
}
type <- match.arg(type)
msg <- paste0("`", what, "` is ", type, " as of shiny ", version, ".")
if (!is.null(with)) {
msg <- paste0(msg, "\n", "Please use `", with, "` instead.")
}
if (!is.null(details)) {
msg <- paste0(msg, "\n", details)
}
# lifecycle::deprecate_soft(when, what, with = with, details = details, id = id, env = env)
rlang::inform(message = msg, .frequency = "always", .frequency_id = msg, .file = stderr())
}
deprecatedEnvQuotedMessage <- function() {
if (!in_devmode()) return(invisible())
if (is_false(getOption("shiny.deprecation.messages"))) return(invisible())
# Capture calling function
grandparent_call <- sys.call(-2)
# Turn language into user friendly string
grandparent_txt <- paste0(utils::capture.output({grandparent_call}), collapse = "\n")
msg <- paste0(
"The `env` and `quoted` arguments are deprecated as of shiny 1.7.0.",
" Please use quosures from `rlang` instead.\n",
"See <https://github.com/rstudio/shiny/issues/3108> for more information.\n",
"Function call:\n",
grandparent_txt
)
# Call less often as users do not have much control over this warning
rlang::inform(message = msg, .frequency = "regularly", .frequency_id = msg, .file = stderr())
}
#' Create disk cache (deprecated)
#'
#' @param exec_missing Deprecated.
#' @inheritParams cachem::cache_disk
#' @keywords internal
#' @export
diskCache <- function(
dir = NULL,
max_size = 500 * 1024 ^ 2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
destroy_on_finalize = FALSE,
missing = key_missing(),
exec_missing = deprecated(),
logfile = NULL
) {
shinyDeprecated("1.6.0", "diskCache()", "cachem::cache_disk()")
if (is_present(exec_missing)) {
shinyDeprecated("1.6.0", "diskCache(exec_missing =)")
}
cachem::cache_disk(
dir = dir,
max_size = max_size,
max_age = max_age,
max_n = max_n,
evict = evict,
destroy_on_finalize = destroy_on_finalize,
missing = missing,
logfile = logfile
)
}
#' Create memory cache (deprecated)
#'
#' @param exec_missing Deprecated.
#' @inheritParams cachem::cache_mem
#' @keywords internal
#' @export
memoryCache <- function(
max_size = 200 * 1024 ^ 2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
missing = key_missing(),
exec_missing = deprecated(),
logfile = NULL)
{
shinyDeprecated("1.6.0", "diskCache()", "cachem::cache_mem()")
if (is_present(exec_missing)) {
shinyDeprecated("1.6.0", "diskCache(exec_missing =)")
}
cachem::cache_mem(
max_size = max_size,
max_age = max_age,
max_n = max_n,
evict = evict,
missing = missing,
logfile = logfile
)
}

369
R/devmode.R Normal file
View File

@@ -0,0 +1,369 @@
#' Shiny Developer Mode
#'
#' @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
#' deprecated functions and options.
#'
#' Shiny Developer Mode can be enabled by calling `devmode(TRUE)` and disabled
#' by calling `devmode(FALSE)`.
#'
#' Please see the function descriptions for more details.
#'
#' @describeIn devmode Function to set two options to enable/disable Shiny
#' Developer Mode and Developer messages
#' @param devmode Logical value which should be set to `TRUE` to enable Shiny
#' Developer Mode
#' @param verbose Logical value which should be set to `TRUE` display Shiny
#' Developer messages
#' @export
#' @examples
#' # Enable Shiny Developer mode
#' devmode()
#'
devmode <- function(
devmode = getOption("shiny.devmode", TRUE),
verbose = getOption("shiny.devmode.verbose", TRUE)
) {
options(
shiny.devmode = devmode,
shiny.devmode.verbose = verbose
)
}
#' @describeIn devmode Determines if Shiny is in Developer Mode. If the
#' `getOption("shiny.devmode")` is set to `TRUE` and not in testing inside
#' `testthat`, then Shiny Developer Mode is enabled.
#' @section Avoiding direct dependency on shiny:
#'
#' The methods explained in this help file act independently from the rest of
#' Shiny but are included to provide blue prints for your own packages. If
#' your package already has (or is willing to take) a dependency on Shiny, we
#' recommend using the exported Shiny methods for consistent behavior. Note
#' that if you use exported Shiny methods, it will cause the Shiny package to
#' load. This may be undesirable if your code will be used in (for example) R
#' Markdown documents that do not have a Shiny runtime (`runtime: shiny`).
#'
#' If your package can **not** take a dependency on Shiny, we recommending
#' re-implementing these two functions:
#'
#' \enumerate{
#' \item `in_devmode()`:
#'
#' This function should return `TRUE` if `getOption("shiny.devmode")` is set.
#' In addition, we strongly recommend that it also checks to make sure
#' `testthat` is not testing.
#'
#' ```r
#' in_devmode <- function() {
#' isTRUE(getOption("shiny.devmode", FALSE)) &&
#' !identical(Sys.getenv("TESTTHAT"), "true")
#' }
#' ```
#'
#' \item `get_devmode_option(name, default, devmode_default, devmode_message)`:
#'
#' This function is similar to `getOption(name, default)`, but when the option
#' is not set, the default value changes depending on the Dev Mode.
#' `get_devmode_option()` should be implemented as follows:
#'
#' * If not in Dev Mode:
#' * Return `getOption(name, default)`.
#' * If in Dev Mode:
#' * Get the global option `getOption(name)` value.
#' * If the global option value is set:
#' * Return the value.
#' * If the global option value is not set:
#' * Notify the developer that the Dev Mode default value will be used.
#' * Return the Dev Mode default value.
#'
#' When notifying the developer that the default value has changed, we strongly
#' recommend displaying a message (`devmode_message`) to `stderr()` once every 8
#' hours using [rlang::inform()]. This will keep the author up to date as to
#' which Dev Mode options are being altered. To allow developers a chance to
#' disable Dev Mode messages, the message should be skipped if
#' `getOption("shiny.devmode.verbose", TRUE)` is not `TRUE`.
#'
#' ```r
#' get_devmode_option <- function(name, default = NULL, devmode_default, devmode_message) {
#' if (!in_devmode()) {
#' # Dev Mode disabled, act like `getOption()`
#' return(getOption(name, default = default))
#' }
#'
#' # Dev Mode enabled, update the default value for `getOption()`
#' getOption(name, default = {
#' # Notify developer
#' if (
#' !missing(devmode_message) &&
#' !is.null(devmode_message) &&
#' getOption("shiny.devmode.verbose", TRUE)
#' ) {
#' rlang::inform(
#' message = devmode_message,
#' .frequency = "regularly",
#' .frequency_id = devmode_message,
#' .file = stderr()
#' )
#' }
#'
#' # Return Dev Mode default value `devmode_default`
#' devmode_default
#' })
#' }
#' ```
#' }
#'
#' The remaining functions in this file are used for author convenience and are
#' not recommended for all reimplementation situations.
#' @export
#' @examples
#' in_devmode() # TRUE/FALSE?
#'
in_devmode <- function() {
isTRUE(getOption("shiny.devmode", FALSE)) &&
# !testthat::is_testing()
!identical(Sys.getenv("TESTTHAT"), "true")
}
in_client_devmode <- function() {
# Client-side devmode enables client-side only dev features without local
# devmode. Currently, the main feature is the client-side error console.
isTRUE(getOption("shiny.client_devmode", FALSE))
}
#' @describeIn devmode Temporarily set Shiny Developer Mode and Developer
#' message verbosity
#' @param code Code to execute with the temporary Dev Mode options set
#' @export
#' @examples
#' # Execute code in a temporary shiny dev mode
#' with_devmode(TRUE, in_devmode()) # TRUE
#'
with_devmode <- function(
devmode,
code,
verbose = getOption("shiny.devmode.verbose", TRUE)
) {
withr::with_options(
list(
shiny.devmode = devmode,
shiny.devmode.verbose = verbose
),
code
)
}
#' @describeIn devmode If Shiny Developer Mode and verbosity are enabled,
#' displays a message once every 8 hrs (by default)
#' @param message Developer Mode message to be sent to [rlang::inform()]
#' @param .frequency Frequency of the Developer Mode message used with
#' [rlang::inform()]. Defaults to once every 8 hours.
#' @param .frequency_id [rlang::inform()] message identifier. Defaults to
#' `message`.
#' @param .file Output connection for [rlang::inform()]. Defaults to [stderr()]
#' @param ... Parameters passed to [rlang::inform()]
devmode_inform <- function(
message,
.frequency = "regularly",
.frequency_id = message,
.file = stderr(),
...
) {
if (!(
in_devmode() &&
isTRUE(getOption("shiny.devmode.verbose", TRUE))
)) {
return()
}
if (is.null(message)) {
return()
}
rlang::inform(
message = paste0("shiny devmode - ", message),
.frequency = .frequency,
.frequency_id = .frequency_id,
.file = .file,
...
)
}
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
#' authors to write one message in a single location.
#'
#' For example, the following Shiny Developer Mode options are registered:
#'
#' ```r
#' # Reload the Shiny app when a sourced R file changes
#' register_devmode_option(
#' "shiny.autoreload",
#' "Turning on shiny autoreload. To disable, call `options(shiny.autoreload = FALSE)`",
#' devmode_default = TRUE
#' )
#'
#' # Use the unminified Shiny JavaScript file, `shiny.js`
#' register_devmode_option(
#' "shiny.minified",
#' "Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`",
#' devmode_default = FALSE
#' )
#'
#' # Display the full stack trace when errors occur during Shiny app execution
#' register_devmode_option(
#' "shiny.fullstacktrace",
#' "Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`",
#' devmode_default = TRUE
#' )
#' ```
#'
#' Other known, non-Shiny Developer Mode options:
#'
#' * Sass:
#' ```r
#' # Display the full stack trace when errors occur during Shiny app execution
#' register_devmode_option(
#' "sass.cache",
#' "Turning off sass cache. To use default caching, call `options(sass.cache = TRUE)`",
#' devmode_default = FALSE
#' )
#' ```
#'
#' @param name Name of option to look for in `options()`
#' @param default Default value to return if `in_devmode()` returns
#' `TRUE` and the specified option is not set in [`options()`].
#' @param devmode_message Message to display once every 8 hours when utilizing
#' the `devmode_default` value. If `devmode_message` is missing, the
#' registered `devmode_message` value be used.
#' @param devmode_default Default value to return if `in_devmode()` returns
#' `TRUE` and the specified option is not set in [`options()`]. For
#' `get_devmode_option()`, if `devmode_default` is missing, the
#' registered `devmode_default` value will be used.
#' @export
#' @examples
#' # Ex: Within shiny, we register the option "shiny.minified"
#' # to default to `FALSE` when in Dev Mode
#' \dontrun{register_devmode_option(
#' "shiny.minified",
#' devmode_message = paste0(
#' "Using full shiny javascript file. ",
#' "To use the minified version, call `options(shiny.minified = TRUE)`"
#' ),
#' devmode_default = FALSE
#' )}
#'
register_devmode_option <- function(
name,
devmode_message = NULL,
devmode_default = NULL
) {
if (!is.null(devmode_message)) {
stopifnot(length(devmode_message) == 1 && is.character(devmode_message))
}
registered_devmode_options$set(
name,
list(devmode_default = devmode_default, devmode_message = devmode_message)
)
}
#' @describeIn devmode Provides a consistent way to change the expected
#' [getOption()] behavior when Developer Mode is enabled. This method is very
#' similar to [getOption()] where the globally set option takes precedence.
#' See section "Avoiding direct dependency on shiny" for
#' `get_devmode_option()` implementation details.
#'
#' **Package developers:** Register your Dev Mode option using
#' `register_devmode_option()` to avoid supplying the same `devmode_default`
#' and `devmode_message` values throughout your package. (This requires a
#' \pkg{shiny} dependency.)
#' @export
#' @examples
#' # Used within `shiny::runApp(launch.browser)`
#' get_devmode_option("shiny.minified", TRUE) # TRUE if Dev mode is off
#' is_minified <- with_devmode(TRUE, {
#' get_devmode_option("shiny.minified", TRUE)
#' })
#' is_minified # FALSE
#'
get_devmode_option <- function(
name,
default = NULL,
devmode_default = missing_arg(),
devmode_message = missing_arg()
) {
getOption(
name,
local({
if (!in_devmode()) {
# typical case
return(default)
}
info <- registered_devmode_options$get(name)
if (is.null(info)) {
# Not registered,
# Warn and return default value
rlang::warn(
message = paste0(
"`get_devmode_option(name)` could not find `name = \"", name, "\"`. ",
"Returning `default` value"
)
)
return(default)
}
# display message
devmode_inform(
maybe_missing(
# use provided `devmode_message` value
devmode_message,
# If `devmode_message` is missing, display registered `devmode_message`
default = info$devmode_message
)
)
# return value
maybe_missing(
# use provided `devmode_default` value
devmode_default,
# if `devmode_default` is missing, provide registered `devmode_default`
default = info$devmode_default
)
})
)
}
on_load({
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.fullstacktrace",
"Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`",
TRUE
)
})

338
R/extended-task.R Normal file
View File

@@ -0,0 +1,338 @@
#' Task or computation that proceeds in the background
#'
#' @description In normal Shiny reactive code, whenever an observer, calc, or
#' output is busy computing, it blocks the current session from receiving any
#' inputs or attempting to proceed with any other computation related to that
#' session.
#'
#' The `ExtendedTask` class allows you to have an expensive operation that is
#' started by a reactive effect, and whose (eventual) results can be accessed
#' by a regular observer, calc, or output; but during the course of the
#' operation, the current session is completely unblocked, allowing the user
#' to continue using the rest of the app while the operation proceeds in the
#' background.
#'
#' Note that each `ExtendedTask` object does not represent a _single
#' invocation_ of its long-running function. Rather, it's an object that is
#' used to invoke the function with different arguments, keeps track of
#' whether an invocation is in progress, and provides ways to get at the
#' current status or results of the operation. A single `ExtendedTask` object
#' does not permit overlapping invocations: if the `invoke()` method is called
#' before the previous `invoke()` is completed, the new invocation will not
#' begin until the previous invocation has completed.
#'
#' @section `ExtendedTask` versus asynchronous reactives:
#'
#' Shiny has long supported [using
#' \{promises\}](https://rstudio.github.io/promises/articles/promises_06_shiny.html)
#' to write asynchronous observers, calcs, or outputs. You may be wondering
#' what the differences are between those techniques and this class.
#'
#' Asynchronous observers, calcs, and outputs are not--and have never
#' been--designed to let a user start a long-running operation, while keeping
#' that very same (browser) session responsive to other interactions. Instead,
#' they unblock other sessions, so you can take a long-running operation that
#' would normally bring the entire R process to a halt and limit the blocking
#' to just the session that started the operation. (For more details, see the
#' section on ["The Flush
#' Cycle"](https://rstudio.github.io/promises/articles/promises_06_shiny.html#the-flush-cycle).)
#'
#' `ExtendedTask`, on the other hand, invokes an asynchronous function (that
#' is, a function that quickly returns a promise) and allows even that very
#' session to immediately unblock and carry on with other user interactions.
#'
#' @section OpenTelemetry Integration:
#'
#' When an `ExtendedTask` is created, if OpenTelemetry tracing is enabled for
#' `"reactivity"` (see [withOtelCollect()]), the `ExtendedTask` will record
#' spans for each invocation of the task. The tracing level at `invoke()` time
#' does not affect whether spans are recorded; only the tracing level when
#' calling `ExtendedTask$new()` matters.
#'
#' The OTel span will be named based on the label created from the variable the
#' `ExtendedTask` is assigned to. If no label can be determined, the span will
#' be named `<anonymous>`. Similar to other Shiny OpenTelemetry spans, the span
#' will also include source reference attributes and session ID attributes.
#'
#' ```r
#' withOtelCollect("all", {
#' my_task <- ExtendedTask$new(function(...) { ... })
#' })
#'
#' # Span recorded for this invocation: ExtendedTask my_task
#' my_task$invoke(...)
#' ```
#'
#' @examplesIf rlang::is_interactive() && rlang::is_installed("mirai")
#' library(shiny)
#' library(bslib)
#' library(mirai)
#'
#' # Set background processes for running tasks
#' daemons(1)
#' # Reset when the app is stopped
#' onStop(function() daemons(0))
#'
#' ui <- page_fluid(
#' titlePanel("Extended Task Demo"),
#' p(
#' 'Click the button below to perform a "calculation"',
#' "that takes a while to perform."
#' ),
#' input_task_button("recalculate", "Recalculate"),
#' p(textOutput("result"))
#' )
#'
#' server <- function(input, output) {
#' rand_task <- ExtendedTask$new(function() {
#' mirai(
#' {
#' # Slow operation goes here
#' Sys.sleep(2)
#' sample(1:100, 1)
#' }
#' )
#' })
#'
#' # Make button state reflect task.
#' # If using R >=4.1, you can do this instead:
#' # rand_task <- ExtendedTask$new(...) |> bind_task_button("recalculate")
#' bind_task_button(rand_task, "recalculate")
#'
#' observeEvent(input$recalculate, {
#' # Invoke the extended in an observer
#' rand_task$invoke()
#' })
#'
#' output$result <- renderText({
#' # React to updated results when the task completes
#' number <- rand_task$result()
#' paste0("Your number is ", number, ".")
#' })
#' }
#'
#' shinyApp(ui, server)
#'
#' @export
ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
public = list(
#' @description
#' Creates a new `ExtendedTask` object. `ExtendedTask` should generally be
#' created either at the top of a server function, or at the top of a module
#' server function.
#'
#' @param func The long-running operation to execute. This should be an
#' asynchronous function, meaning, it should use the
#' [\{promises\}](https://rstudio.github.io/promises/) package, most
#' likely in conjunction with the
#' [\{mirai\}](https://mirai.r-lib.org) or
#' [\{future\}](https://rstudio.github.io/promises/articles/promises_04_futures.html)
#' package. (In short, the return value of `func` should be a
#' [`mirai`][mirai::mirai()], [`Future`][future::future()], `promise`,
#' or something else that [promises::as.promise()] understands.)
#'
#' It's also important that this logic does not read from any
#' reactive inputs/sources, as inputs may change after the function is
#' invoked; instead, if the function needs to access reactive inputs, it
#' should take parameters and the caller of the `invoke()` method should
#' read reactive inputs and pass them as arguments.
initialize = function(func) {
private$func <- func
# Do not show these private reactive values in otel spans
with_no_otel_collect({
private$rv_status <- reactiveVal("initial", label = "ExtendedTask$private$status")
private$rv_value <- reactiveVal(NULL, label = "ExtendedTask$private$value")
private$rv_error <- reactiveVal(NULL, label = "ExtendedTask$private$error")
})
private$invocation_queue <- fastmap::fastqueue()
domain <- getDefaultReactiveDomain()
# Set a label for the reactive values for easier debugging
# Go up an extra sys.call() to get the user's call to ExtendedTask$new()
# The first sys.call() is to `initialize(...)`
call_srcref <- get_call_srcref(-1)
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = "<anonymous>"
)
private$otel_span_label <- otel_span_label_extended_task(label, domain = domain)
private$otel_log_label_add_to_queue <- otel_log_label_extended_task_add_to_queue(label, domain = domain)
private$otel_attrs <- c(
otel_srcref_attributes(call_srcref, "ExtendedTask"),
otel_session_id_attrs(domain)
) %||% list()
# Capture this value at init-time, not run-time
# This way, the span is only created if otel was enabled at time of creation... just like other spans
private$is_recording_otel <- has_otel_collect("reactivity")
},
#' @description
#' Starts executing the long-running operation. If this `ExtendedTask` is
#' already running (meaning, a previous call to `invoke()` is not yet
#' complete) then enqueues this invocation until after the current
#' invocation, and any already-enqueued invocation, completes.
#'
#' @param ... Parameters to use for this invocation of the underlying
#' function. If reactive inputs are needed by the underlying function,
#' they should be read by the caller of `invoke` and passed in as
#' arguments.
invoke = function(...) {
args <- rlang::dots_list(..., .ignore_empty = "none")
call <- rlang::caller_call(n = 0)
if (
isolate(private$rv_status()) == "running" ||
private$invocation_queue$size() > 0
) {
otel_log(
private$otel_log_add_to_queue_label,
severity = "debug",
attributes = c(
private$otel_attrs,
list(
queue_size = private$invocation_queue$size() + 1L
)
)
)
private$invocation_queue$add(list(args = args, call = call))
} else {
if (private$is_recording_otel) {
private$otel_span <- start_otel_span(
private$otel_span_label,
attributes = private$otel_attrs
)
otel::local_active_span(private$otel_span)
}
private$do_invoke(args, call = call)
}
invisible(NULL)
},
#' @description
#' This is a reactive read that invalidates the caller when the task's
#' status changes.
#'
#' Returns one of the following values:
#'
#' * `"initial"`: This `ExtendedTask` has not yet been invoked
#' * `"running"`: An invocation is currently running
#' * `"success"`: An invocation completed successfully, and a value can be
#' retrieved via the `result()` method
#' * `"error"`: An invocation completed with an error, which will be
#' re-thrown if you call the `result()` method
status = function() {
private$rv_status()
},
#' @description
#' Attempts to read the results of the most recent invocation. This is a
#' reactive read that invalidates as the task's status changes.
#'
#' The actual behavior differs greatly depending on the current status of
#' the task:
#'
#' * `"initial"`: Throws a silent error (like [`req(FALSE)`][req()]). If
#' this happens during output rendering, the output will be blanked out.
#' * `"running"`: Throws a special silent error that, if it happens during
#' output rendering, makes the output appear "in progress" until further
#' notice.
#' * `"success"`: Returns the return value of the most recent invocation.
#' * `"error"`: Throws whatever error was thrown by the most recent
#' invocation.
#'
#' This method is intended to be called fairly naively by any output or
#' reactive expression that cares about the output--you just have to be
#' aware that if the result isn't ready for whatever reason, processing will
#' stop in much the same way as `req(FALSE)` does, but when the result is
#' ready you'll get invalidated, and when you run again the result should be
#' there.
#'
#' Note that the `result()` method is generally not meant to be used with
#' [observeEvent()], [eventReactive()], [bindEvent()], or [isolate()] as the
#' invalidation will be ignored.
result = function() {
switch (private$rv_status(),
running = req(FALSE, cancelOutput = "progress"),
success = if (private$rv_value()$visible) {
private$rv_value()$value
} else {
invisible(private$rv_value()$value)
},
error = stop(private$rv_error()),
# default case (initial, cancelled)
req(FALSE)
)
}
),
private = list(
func = NULL,
# reactive value with "initial"|"running"|"success"|"error"
rv_status = NULL,
rv_value = NULL,
rv_error = NULL,
invocation_queue = NULL,
otel_span_label = NULL,
otel_log_label_add_to_queue = NULL,
otel_attrs = list(),
is_recording_otel = FALSE,
otel_span = NULL,
do_invoke = function(args, call = NULL) {
private$rv_status("running")
private$rv_value(NULL)
private$rv_error(NULL)
p <- promise_resolve(
maskReactiveContext(do.call(private$func, args))
)
p <- promises::then(
p,
onFulfilled = function(value, .visible) {
if (is_otel_span(private$otel_span)) {
private$otel_span$end(status_code = "ok")
private$otel_span <- NULL
}
private$on_success(list(value = value, visible = .visible))
},
onRejected = function(error) {
if (is_otel_span(private$otel_span)) {
private$otel_span$end(status_code = "error")
private$otel_span <- NULL
}
private$on_error(error, call = call)
}
)
promises::finally(p, onFinally = function() {
if (private$invocation_queue$size() > 0) {
next_call <- private$invocation_queue$remove()
private$do_invoke(next_call$args, next_call$call)
}
})
invisible(NULL)
},
on_error = function(err, call = NULL) {
cli::cli_warn(
"ERROR: An error occurred when invoking the ExtendedTask.",
parent = err,
call = call
)
private$rv_status("error")
private$rv_error(err)
},
on_success = function(value) {
private$rv_status("success")
private$rv_value(value)
}
)
)

View File

@@ -1,445 +0,0 @@
font_awesome_brands <- c(
"500px",
"accessible-icon",
"accusoft",
"acquisitions-incorporated",
"adn",
"adobe",
"adversal",
"affiliatetheme",
"airbnb",
"algolia",
"alipay",
"amazon",
"amazon-pay",
"amilia",
"android",
"angellist",
"angrycreative",
"angular",
"app-store",
"app-store-ios",
"apper",
"apple",
"apple-pay",
"artstation",
"asymmetrik",
"atlassian",
"audible",
"autoprefixer",
"avianex",
"aviato",
"aws",
"bandcamp",
"battle-net",
"behance",
"behance-square",
"bimobject",
"bitbucket",
"bitcoin",
"bity",
"black-tie",
"blackberry",
"blogger",
"blogger-b",
"bluetooth",
"bluetooth-b",
"bootstrap",
"btc",
"buffer",
"buromobelexperte",
"buy-n-large",
"buysellads",
"canadian-maple-leaf",
"cc-amazon-pay",
"cc-amex",
"cc-apple-pay",
"cc-diners-club",
"cc-discover",
"cc-jcb",
"cc-mastercard",
"cc-paypal",
"cc-stripe",
"cc-visa",
"centercode",
"centos",
"chrome",
"chromecast",
"cloudscale",
"cloudsmith",
"cloudversify",
"codepen",
"codiepie",
"confluence",
"connectdevelop",
"contao",
"cotton-bureau",
"cpanel",
"creative-commons",
"creative-commons-by",
"creative-commons-nc",
"creative-commons-nc-eu",
"creative-commons-nc-jp",
"creative-commons-nd",
"creative-commons-pd",
"creative-commons-pd-alt",
"creative-commons-remix",
"creative-commons-sa",
"creative-commons-sampling",
"creative-commons-sampling-plus",
"creative-commons-share",
"creative-commons-zero",
"critical-role",
"css3",
"css3-alt",
"cuttlefish",
"d-and-d",
"d-and-d-beyond",
"dailymotion",
"dashcube",
"delicious",
"deploydog",
"deskpro",
"dev",
"deviantart",
"dhl",
"diaspora",
"digg",
"digital-ocean",
"discord",
"discourse",
"dochub",
"docker",
"draft2digital",
"dribbble",
"dribbble-square",
"dropbox",
"drupal",
"dyalog",
"earlybirds",
"ebay",
"edge",
"elementor",
"ello",
"ember",
"empire",
"envira",
"erlang",
"ethereum",
"etsy",
"evernote",
"expeditedssl",
"facebook",
"facebook-f",
"facebook-messenger",
"facebook-square",
"fantasy-flight-games",
"fedex",
"fedora",
"figma",
"firefox",
"firefox-browser",
"first-order",
"first-order-alt",
"firstdraft",
"flickr",
"flipboard",
"fly",
"font-awesome",
"font-awesome-alt",
"font-awesome-flag",
"font-awesome-logo-full",
"fonticons",
"fonticons-fi",
"fort-awesome",
"fort-awesome-alt",
"forumbee",
"foursquare",
"free-code-camp",
"freebsd",
"fulcrum",
"galactic-republic",
"galactic-senate",
"get-pocket",
"gg",
"gg-circle",
"git",
"git-alt",
"git-square",
"github",
"github-alt",
"github-square",
"gitkraken",
"gitlab",
"gitter",
"glide",
"glide-g",
"gofore",
"goodreads",
"goodreads-g",
"google",
"google-drive",
"google-play",
"google-plus",
"google-plus-g",
"google-plus-square",
"google-wallet",
"gratipay",
"grav",
"gripfire",
"grunt",
"gulp",
"hacker-news",
"hacker-news-square",
"hackerrank",
"hips",
"hire-a-helper",
"hooli",
"hornbill",
"hotjar",
"houzz",
"html5",
"hubspot",
"ideal",
"imdb",
"instagram",
"instagram-square",
"intercom",
"internet-explorer",
"invision",
"ioxhost",
"itch-io",
"itunes",
"itunes-note",
"java",
"jedi-order",
"jenkins",
"jira",
"joget",
"joomla",
"js",
"js-square",
"jsfiddle",
"kaggle",
"keybase",
"keycdn",
"kickstarter",
"kickstarter-k",
"korvue",
"laravel",
"lastfm",
"lastfm-square",
"leanpub",
"less",
"line",
"linkedin",
"linkedin-in",
"linode",
"linux",
"lyft",
"magento",
"mailchimp",
"mandalorian",
"markdown",
"mastodon",
"maxcdn",
"mdb",
"medapps",
"medium",
"medium-m",
"medrt",
"meetup",
"megaport",
"mendeley",
"microblog",
"microsoft",
"mix",
"mixcloud",
"mixer",
"mizuni",
"modx",
"monero",
"napster",
"neos",
"nimblr",
"node",
"node-js",
"npm",
"ns8",
"nutritionix",
"odnoklassniki",
"odnoklassniki-square",
"old-republic",
"opencart",
"openid",
"opera",
"optin-monster",
"orcid",
"osi",
"page4",
"pagelines",
"palfed",
"patreon",
"paypal",
"penny-arcade",
"periscope",
"phabricator",
"phoenix-framework",
"phoenix-squadron",
"php",
"pied-piper",
"pied-piper-alt",
"pied-piper-hat",
"pied-piper-pp",
"pied-piper-square",
"pinterest",
"pinterest-p",
"pinterest-square",
"playstation",
"product-hunt",
"pushed",
"python",
"qq",
"quinscape",
"quora",
"r-project",
"raspberry-pi",
"ravelry",
"react",
"reacteurope",
"readme",
"rebel",
"red-river",
"reddit",
"reddit-alien",
"reddit-square",
"redhat",
"renren",
"replyd",
"researchgate",
"resolving",
"rev",
"rocketchat",
"rockrms",
"safari",
"salesforce",
"sass",
"schlix",
"scribd",
"searchengin",
"sellcast",
"sellsy",
"servicestack",
"shirtsinbulk",
"shopify",
"shopware",
"simplybuilt",
"sistrix",
"sith",
"sketch",
"skyatlas",
"skype",
"slack",
"slack-hash",
"slideshare",
"snapchat",
"snapchat-ghost",
"snapchat-square",
"soundcloud",
"sourcetree",
"speakap",
"speaker-deck",
"spotify",
"squarespace",
"stack-exchange",
"stack-overflow",
"stackpath",
"staylinked",
"steam",
"steam-square",
"steam-symbol",
"sticker-mule",
"strava",
"stripe",
"stripe-s",
"studiovinari",
"stumbleupon",
"stumbleupon-circle",
"superpowers",
"supple",
"suse",
"swift",
"symfony",
"teamspeak",
"telegram",
"telegram-plane",
"tencent-weibo",
"the-red-yeti",
"themeco",
"themeisle",
"think-peaks",
"trade-federation",
"trello",
"tripadvisor",
"tumblr",
"tumblr-square",
"twitch",
"twitter",
"twitter-square",
"typo3",
"uber",
"ubuntu",
"uikit",
"umbraco",
"uniregistry",
"unity",
"untappd",
"ups",
"usb",
"usps",
"ussunnah",
"vaadin",
"viacoin",
"viadeo",
"viadeo-square",
"viber",
"vimeo",
"vimeo-square",
"vimeo-v",
"vine",
"vk",
"vnv",
"vuejs",
"waze",
"weebly",
"weibo",
"weixin",
"whatsapp",
"whatsapp-square",
"whmcs",
"wikipedia-w",
"windows",
"wix",
"wizards-of-the-coast",
"wolf-pack-battalion",
"wordpress",
"wordpress-simple",
"wpbeginner",
"wpexplorer",
"wpforms",
"wpressr",
"xbox",
"xing",
"xing-square",
"y-combinator",
"yahoo",
"yammer",
"yandex",
"yandex-international",
"yarn",
"yelp",
"yoast",
"youtube",
"youtube-square",
"zhihu"
)

View File

@@ -1,70 +1,31 @@
# A scope where we can put mutable global state
.globals <- new.env(parent = emptyenv())
register_s3_method <- function(pkg, generic, class, fun = NULL) {
stopifnot(is.character(pkg), length(pkg) == 1)
stopifnot(is.character(generic), length(generic) == 1)
stopifnot(is.character(class), length(class) == 1)
if (is.null(fun)) {
fun <- get(paste0(generic, ".", class), envir = parent.frame())
} else {
stopifnot(is.function(fun))
}
if (pkg %in% loadedNamespaces()) {
registerS3method(generic, class, fun, envir = asNamespace(pkg))
}
# Always register hook in case pkg is loaded at some
# point the future (or, potentially, but less commonly,
# unloaded & reloaded)
setHook(
packageEvent(pkg, "onLoad"),
function(...) {
registerS3method(generic, class, fun, envir = asNamespace(pkg))
}
)
}
register_upgrade_message <- function(pkg, version) {
msg <- sprintf(
"This version of Shiny is designed to work with '%s' >= %s.
Please upgrade via install.packages('%s').",
pkg, version, pkg
)
if (pkg %in% loadedNamespaces() && !is_available(pkg, version)) {
packageStartupMessage(msg)
}
# Always register hook in case pkg is loaded at some
# point the future (or, potentially, but less commonly,
# unloaded & reloaded)
setHook(
packageEvent(pkg, "onLoad"),
function(...) {
if (!is_available(pkg, version)) packageStartupMessage(msg)
}
)
}
.onLoad <- function(libname, pkgname) {
# R's lazy-loading package scheme causes the private seed to be cached in the
# package itself, making our PRNG completely deterministic. This line resets
# the private seed during load.
withPrivateSeed(set.seed(NULL))
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.
register_s3_method("knitr", "knit_print", "reactive")
register_s3_method("knitr", "knit_print", "shiny.appobj")
register_s3_method("knitr", "knit_print", "shiny.render.function")
# Shiny 1.4.0 bumps jQuery 1.x to 3.x, which caused a problem
# with static-rendering of htmlwidgets, and htmlwidgets 1.5
# includes a fix for this problem
# https://github.com/rstudio/shiny/issues/2630
register_upgrade_message("htmlwidgets", 1.5)
s3_register("knitr::knit_print", "reactive")
s3_register("knitr::knit_print", "shiny.appobj")
s3_register("knitr::knit_print", "shiny.render.function")
}
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)
}
on_load({
IS_SHINY_LOCAL_PKG <- exists(".__DEVTOOLS__")
})

197
R/graph.R
View File

@@ -1,32 +1,3 @@
# Check that the version of an suggested package satisfies the requirements
#
# @param package The name of the suggested package
# @param version The version of the package
check_suggested <- function(package, version = NULL) {
if (is_available(package, version)) {
return()
}
msg <- paste0(
sQuote(package),
if (is.na(version %OR% NA)) "" else paste0("(>= ", version, ")"),
" must be installed for this functionality."
)
if (interactive()) {
message(msg, "\nWould you like to install it?")
if (utils::menu(c("Yes", "No")) == 1) {
return(utils::install.packages(package))
}
}
stop(msg, call. = FALSE)
}
# domain is like session
@@ -48,7 +19,7 @@ reactIdStr <- function(num) {
#' dependencies and execution in your application.
#'
#' To use the reactive log visualizer, start with a fresh R session and
#' run the command `options(shiny.reactlog=TRUE)`; then launch your
#' run the command `reactlog::reactlog_enable()`; then launch your
#' application in the usual way (e.g. using [runApp()]). At
#' any time you can hit Ctrl+F3 (or for Mac users, Command+F3) in your
#' web browser to launch the reactive log visualization.
@@ -71,41 +42,62 @@ reactIdStr <- function(num) {
#' call `reactlogShow()` explicitly.
#'
#' For security and performance reasons, do not enable
#' `shiny.reactlog` in production environments. When the option is
#' enabled, it's possible for any user of your app to see at least some
#' of the source code of your reactive expressions and observers.
#' `options(shiny.reactlog=TRUE)` (or `reactlog::reactlog_enable()`) in
#' production environments. When the option is enabled, it's possible
#' for any user of your app to see at least some of the source code of
#' your reactive expressions and observers. In addition, reactlog
#' should be considered a memory leak as it will constantly grow and
#' will never reset until the R session is restarted.
#'
#' @name reactlog
NULL
#' @describeIn reactlog Return a list of reactive information. Can be used in conjunction with
#' [reactlog::reactlog_show] to later display the reactlog graph.
#' @describeIn reactlog Return a list of reactive information. Can be used in
#' conjunction with [reactlog::reactlog_show] to later display the reactlog
#' graph.
#' @export
reactlog <- function() {
rLog$asList()
}
#' @describeIn reactlog Display a full reactlog graph for all sessions.
#' @inheritParams reactlog::reactlog_show
#' @param time A boolean that specifies whether or not to display the
#' time that each reactive takes to calculate a result.
#' @export
reactlogShow <- function(time = TRUE) {
check_reactlog()
reactlog::reactlog_show(reactlog(), time = time)
}
#' @describeIn reactlog This function is deprecated. You should use [reactlogShow()]
#' @export
# legacy purposes
showReactLog <- function(time = TRUE) {
shinyDeprecated(new = "`reactlogShow`", version = "1.2.0")
reactlogShow(time = time)
}
#' @describeIn reactlog Resets the entire reactlog stack. Useful for debugging and removing all prior reactive history.
#' @describeIn reactlog Resets the entire reactlog stack. Useful for debugging
#' and removing all prior reactive history.
#' @export
reactlogReset <- function() {
rLog$reset()
}
#' @describeIn reactlog Adds "mark" entry into the reactlog stack. This is
#' useful for programmatically adding a marked entry in the reactlog, rather
#' than using your keyboard's key combination.
#'
#' For example, we can _mark_ the reactlog at the beginning of an
#' `observeEvent`'s calculation:
#' ```r
#' observeEvent(input$my_event_trigger, {
#' # Add a mark in the reactlog
#' reactlogAddMark()
#' # Run your regular event reaction code here...
#' ....
#' })
#' ```
#' @param session The Shiny session to assign the mark to. Defaults to the
#' current session.
#' @export
reactlogAddMark <- function(session = getDefaultReactiveDomain()) {
rLog$userMark(session)
}
# called in "/reactlog" middleware
renderReactlog <- function(sessionToken = NULL, time = TRUE) {
check_reactlog()
@@ -115,28 +107,15 @@ renderReactlog <- function(sessionToken = NULL, time = TRUE) {
time = time
)
}
check_reactlog <- function() {
check_suggested("reactlog", reactlog_version())
}
# read reactlog version from description file
# prevents version mismatch in code and description file
reactlog_version <- function() {
desc <- read.dcf(system.file("DESCRIPTION", package = "shiny", mustWork = TRUE))
suggests <- desc[1,"Suggests"][[1]]
suggests_pkgs <- strsplit(suggests, "\n")[[1]]
reactlog_info <- suggests_pkgs[grepl("reactlog", suggests_pkgs)]
if (length(reactlog_info) == 0) {
stop("reactlog can not be found in shiny DESCRIPTION file")
if (!is_installed("reactlog", reactlog_min_version)) {
rlang::check_installed("reactlog", reactlog_min_version)
}
reactlog_info <- sub("^[^\\(]*\\(", "", reactlog_info)
reactlog_info <- sub("\\)[^\\)]*$", "", reactlog_info)
reactlog_info <- sub("^[>= ]*", "", reactlog_info)
package_version(reactlog_info)
}
# Should match the (suggested) version in DESCRIPTION file
reactlog_min_version <- "1.0.0"
RLog <- R6Class(
"RLog",
@@ -144,7 +123,6 @@ RLog <- R6Class(
private = list(
option = "shiny.reactlog",
msgOption = "shiny.reactlog.console",
appendEntry = function(domain, logEntry) {
if (self$isLogging()) {
sessionToken <- if (is.null(domain)) NULL else domain$token
@@ -159,35 +137,33 @@ RLog <- R6Class(
public = list(
msg = "<MessageLogger>",
logStack = "<Stack>",
noReactIdLabel = "NoCtxReactId",
noReactId = reactIdStr("NoCtxReactId"),
dummyReactIdLabel = "DummyReactId",
dummyReactId = reactIdStr("DummyReactId"),
asList = function() {
ret <- self$logStack$as_list()
attr(ret, "version") <- "1"
ret
},
ctxIdStr = function(ctxId) {
if (is.null(ctxId) || identical(ctxId, "")) return(NULL)
if (is.null(ctxId) || identical(ctxId, "")) {
return(NULL)
}
paste0("ctx", ctxId)
},
namesIdStr = function(reactId) {
paste0("names(", reactId, ")")
},
asListIdStr = function(reactId) {
paste0("as.list(", reactId, ")")
paste0("reactiveValuesToList(", reactId, ")")
},
asListAllIdStr = function(reactId) {
paste0("as.list(", reactId, ", all.names = TRUE)")
paste0("reactiveValuesToList(", reactId, ", all.names = TRUE)")
},
keyIdStr = function(reactId, key) {
paste0(reactId, "$", key)
},
valueStr = function(value, n = 200) {
if (!self$isLogging()) {
# return a placeholder string to avoid calling str
@@ -197,10 +173,9 @@ RLog <- R6Class(
# only capture the first level of the object
utils::capture.output(utils::str(value, max.level = 1))
})
outputTxt <- paste0(output, collapse="\n")
outputTxt <- paste0(output, collapse = "\n")
msg$shortenString(outputTxt, n = n)
},
initialize = function(rlogOption = "shiny.reactlog", msgOption = "shiny.reactlog.console") {
private$option <- rlogOption
private$msgOption <- msgOption
@@ -210,7 +185,7 @@ RLog <- R6Class(
reset = function() {
.globals$reactIdCounter <- 0L
self$logStack <- Stack$new()
self$logStack <- fastmap::faststack()
self$msg <- MessageLogger$new(option = private$msgOption)
# setup dummy and missing react information
@@ -220,7 +195,6 @@ RLog <- R6Class(
isLogging = function() {
isTRUE(getOption(private$option, FALSE))
},
define = function(reactId, value, label, type, domain) {
valueStr <- self$valueStr(value)
if (msg$hasReact(reactId)) {
@@ -251,9 +225,10 @@ RLog <- R6Class(
defineObserver = function(reactId, label, domain) {
self$define(reactId, value = NULL, label, "observer", domain)
},
dependsOn = function(reactId, depOnReactId, ctxId, domain) {
if (is.null(reactId)) return()
if (is.null(reactId)) {
return()
}
ctxId <- ctxIdStr(ctxId)
msg$log("dependsOn:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId))
private$appendEntry(domain, list(
@@ -266,7 +241,6 @@ RLog <- R6Class(
dependsOnKey = function(reactId, depOnReactId, key, ctxId, domain) {
self$dependsOn(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain)
},
dependsOnRemove = function(reactId, depOnReactId, ctxId, domain) {
ctxId <- self$ctxIdStr(ctxId)
msg$log("dependsOnRemove:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId))
@@ -280,7 +254,6 @@ RLog <- R6Class(
dependsOnKeyRemove = function(reactId, depOnReactId, key, ctxId, domain) {
self$dependsOnRemove(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain)
},
createContext = function(ctxId, label, type, prevCtxId, domain) {
ctxId <- self$ctxIdStr(ctxId)
prevCtxId <- self$ctxIdStr(prevCtxId)
@@ -291,10 +264,9 @@ RLog <- R6Class(
label = msg$shortenString(label),
type = type,
prevCtxId = prevCtxId,
srcref = as.vector(attr(label, "srcref")), srcfile=attr(label, "srcfile")
srcref = as.vector(attr(label, "srcref")), srcfile = attr(label, "srcfile")
))
},
enter = function(reactId, ctxId, type, domain) {
ctxId <- self$ctxIdStr(ctxId)
if (identical(type, "isolate")) {
@@ -337,7 +309,6 @@ RLog <- R6Class(
))
}
},
valueChange = function(reactId, value, domain) {
valueStr <- self$valueStr(value)
msg$log("valueChange:", msg$reactStr(reactId), msg$valueStr(valueStr))
@@ -359,8 +330,6 @@ RLog <- R6Class(
valueChangeKey = function(reactId, key, value, domain) {
self$valueChange(self$keyIdStr(reactId, key), value, domain)
},
invalidateStart = function(reactId, ctxId, type, domain) {
ctxId <- self$ctxIdStr(ctxId)
if (identical(type, "isolate")) {
@@ -403,7 +372,6 @@ RLog <- R6Class(
))
}
},
invalidateLater = function(reactId, runningCtx, millis, domain) {
msg$log("invalidateLater: ", millis, "ms", msg$reactStr(reactId), msg$ctxStr(runningCtx))
private$appendEntry(domain, list(
@@ -413,14 +381,12 @@ RLog <- R6Class(
millis = millis
))
},
idle = function(domain = NULL) {
msg$log("idle")
private$appendEntry(domain, list(
action = "idle"
))
},
asyncStart = function(domain = NULL) {
msg$log("asyncStart")
private$appendEntry(domain, list(
@@ -433,7 +399,6 @@ RLog <- R6Class(
action = "asyncStop"
))
},
freezeReactiveVal = function(reactId, domain) {
msg$log("freeze:", msg$reactStr(reactId))
private$appendEntry(domain, list(
@@ -444,7 +409,6 @@ RLog <- R6Class(
freezeReactiveKey = function(reactId, key, domain) {
self$freezeReactiveVal(self$keyIdStr(reactId, key), domain)
},
thawReactiveVal = function(reactId, domain) {
msg$log("thaw:", msg$reactStr(reactId))
private$appendEntry(domain, list(
@@ -455,54 +419,60 @@ RLog <- R6Class(
thawReactiveKey = function(reactId, key, domain) {
self$thawReactiveVal(self$keyIdStr(reactId, key), domain)
},
userMark = function(domain = NULL) {
msg$log("userMark")
private$appendEntry(domain, list(
action = "userMark"
))
}
)
)
MessageLogger = R6Class(
MessageLogger <- R6Class(
"MessageLogger",
portable = FALSE,
public = list(
depth = 0L,
reactCache = list(),
option = "shiny.reactlog.console",
initialize = function(option = "shiny.reactlog.console", depth = 0L) {
if (!missing(depth)) self$depth <- depth
if (!missing(option)) self$option <- option
},
isLogging = function() {
isTRUE(getOption(self$option))
},
isNotLogging = function() {
! isTRUE(getOption(self$option))
!isTRUE(getOption(self$option))
},
depthIncrement = function() {
if (self$isNotLogging()) return(NULL)
if (self$isNotLogging()) {
return(NULL)
}
self$depth <- self$depth + 1L
},
depthDecrement = function() {
if (self$isNotLogging()) return(NULL)
if (self$isNotLogging()) {
return(NULL)
}
self$depth <- self$depth - 1L
},
hasReact = function(reactId) {
if (self$isNotLogging()) return(FALSE)
if (self$isNotLogging()) {
return(FALSE)
}
!is.null(self$getReact(reactId))
},
getReact = function(reactId, force = FALSE) {
if (identical(force, FALSE) && self$isNotLogging()) return(NULL)
if (identical(force, FALSE) && self$isNotLogging()) {
return(NULL)
}
self$reactCache[[reactId]]
},
setReact = function(reactObj, force = FALSE) {
if (identical(force, FALSE) && self$isNotLogging()) return(NULL)
if (identical(force, FALSE) && self$isNotLogging()) {
return(NULL)
}
self$reactCache[[reactObj$reactId]] <- reactObj
},
shortenString = function(txt, n = 250) {
@@ -517,17 +487,21 @@ MessageLogger = R6Class(
return(txt)
},
singleLine = function(txt) {
gsub("[^\\]\\n", "\\\\n", txt)
gsub("([^\\])\\n", "\\1\\\\n", txt)
},
valueStr = function(valueStr) {
paste0(
" '", self$shortenString(self$singleLine(valueStr)), "'"
" '", self$shortenString(self$singleLine(valueStr)), "'"
)
},
reactStr = function(reactId) {
if (self$isNotLogging()) return(NULL)
if (self$isNotLogging()) {
return(NULL)
}
reactInfo <- self$getReact(reactId)
if (is.null(reactInfo)) return(" <UNKNOWN_REACTID>")
if (is.null(reactInfo)) {
return(" <UNKNOWN_REACTID>")
}
paste0(
" ", reactInfo$reactId, ":'", self$shortenString(self$singleLine(reactInfo$label)), "'"
)
@@ -536,11 +510,15 @@ MessageLogger = R6Class(
self$ctxStr(ctxId = NULL, type = type)
},
ctxStr = function(ctxId = NULL, type = NULL) {
if (self$isNotLogging()) return(NULL)
if (self$isNotLogging()) {
return(NULL)
}
self$ctxPrevCtxStr(ctxId = ctxId, prevCtxId = NULL, type = type)
},
ctxPrevCtxStr = function(ctxId = NULL, prevCtxId = NULL, type = NULL, preCtxIdTxt = " in ") {
if (self$isNotLogging()) return(NULL)
if (self$isNotLogging()) {
return(NULL)
}
paste0(
if (!is.null(ctxId)) paste0(preCtxIdTxt, ctxId),
if (!is.null(prevCtxId)) paste0(" from ", prevCtxId),
@@ -548,7 +526,9 @@ MessageLogger = R6Class(
)
},
log = function(...) {
if (self$isNotLogging()) return(NULL)
if (self$isNotLogging()) {
return(NULL)
}
msg <- paste0(
paste0(rep("= ", depth), collapse = ""), "- ", paste0(..., collapse = ""),
collapse = ""
@@ -558,5 +538,6 @@ MessageLogger = R6Class(
)
)
#' @include stack.R
rLog <- RLog$new("shiny.reactlog", "shiny.reactlog.console")
on_load({
rLog <- RLog$new("shiny.reactlog", "shiny.reactlog.console")
})

View File

@@ -14,7 +14,7 @@ NULL
#' depending on the values in the query string / hash (e.g. instead of basing
#' the conditional on an input or a calculated reactive, you can base it on the
#' query string). However, note that, if you're changing the query string / hash
#' programatically from within the server code, you must use
#' programmatically from within the server code, you must use
#' `updateQueryString(_yourNewQueryString_, mode = "push")`. The default
#' `mode` for `updateQueryString` is `"replace"`, which doesn't
#' raise any events, so any observers or reactives that depend on it will

View File

@@ -40,11 +40,14 @@ createWebDependency <- function(dependency, scrubFile = TRUE) {
# Given a Shiny tag object, process singletons and dependencies. Returns a list
# with rendered HTML and dependency objects.
# This implementation is very similar to renderTags(), but ignores
# <head> handling (it should only be used after the user session has started)
processDeps <- function(tags, session) {
ui <- takeSingletons(tags, session$singletons, desingleton=FALSE)$ui
tags <- utils::getFromNamespace("tagify", "htmltools")(tags)
ui <- takeSingletons(tags, session$singletons, desingleton = FALSE)$ui
ui <- surroundSingletons(ui)
dependencies <- lapply(
resolveDependencies(findDependencies(ui)),
resolveDependencies(findDependencies(ui, tagify = FALSE)),
createWebDependency
)
names(dependencies) <- NULL

View File

@@ -1,11 +0,0 @@
#' @import htmltools
#' @export tags p h1 h2 h3 h4 h5 h6 a br div span pre code img strong em hr
#' @export tag tagList tagAppendAttributes tagHasAttribute tagGetAttribute tagAppendChild tagAppendChildren tagSetChildren
#' @export HTML
#' @export includeHTML includeText includeMarkdown includeCSS includeScript
#' @export singleton is.singleton
#' @export validateCssUnit
#' @export htmlTemplate
#' @export suppressDependencies
#' @export withTags
NULL

View File

@@ -1,14 +1,24 @@
#' Create an object representing click options
#' Control interactive plot point events
#'
#' This generates an object representing click options, to be passed as the
#' `click` argument of [imageOutput()] or
#' [plotOutput()].
#' These functions give control over the `click`, `dblClick` and
#' `hover` events generated by [imageOutput()] and [plotOutput()].
#'
#' @param id Input value name. For example, if the value is `"plot_click"`,
#' then the click coordinates will be available as `input$plot_click`.
#' @param clip Should the click area be clipped to the plotting area? If FALSE,
#' then the server will receive click events even when the mouse is outside
#' the plotting area, as long as it is still inside the image.
#' then the event data will be available as `input$plot_click`.
#' @param clip Should the click area be clipped to the plotting area? If
#' `FALSE`, then the server will receive click events even when the mouse is
#' outside the plotting area, as long as it is still inside the image.
#' @param delay For `dblClickOpts()`: the maximum delay (in ms) between a
#' pair clicks for them to be counted as a double-click.
#'
#' For `hoverOpts()`: how long to delay (in ms) when debouncing or throttling
#' before sending the mouse location to the server.
#' @param delayType The type of algorithm for limiting the number of hover
#' events. Use `"throttle"` to limit the number of hover events to one
#' every `delay` milliseconds. Use `"debounce"` to suspend events
#' while the cursor is moving, and wait until the cursor has been at rest for
#' `delay` milliseconds before sending an event.
#' @seealso [brushOpts()] for brushing events.
#' @export
clickOpts <- function(id, clip = TRUE) {
if (is.null(id))
@@ -21,21 +31,8 @@ clickOpts <- function(id, clip = TRUE) {
}
#' Create an object representing double-click options
#'
#' This generates an object representing dobule-click options, to be passed as
#' the `dblclick` argument of [imageOutput()] or
#' [plotOutput()].
#'
#' @param id Input value name. For example, if the value is
#' `"plot_dblclick"`, then the click coordinates will be available as
#' `input$plot_dblclick`.
#' @param clip Should the click area be clipped to the plotting area? If FALSE,
#' then the server will receive double-click events even when the mouse is
#' outside the plotting area, as long as it is still inside the image.
#' @param delay Maximum delay (in ms) between a pair clicks for them to be
#' counted as a double-click.
#' @export
#' @rdname clickOpts
dblclickOpts <- function(id, clip = TRUE, delay = 400) {
if (is.null(id))
stop("id must not be NULL")
@@ -47,28 +44,11 @@ dblclickOpts <- function(id, clip = TRUE, delay = 400) {
)
}
#' Create an object representing hover options
#'
#' This generates an object representing hovering options, to be passed as the
#' `hover` argument of [imageOutput()] or
#' [plotOutput()].
#'
#' @param id Input value name. For example, if the value is `"plot_hover"`,
#' then the hover coordinates will be available as `input$plot_hover`.
#' @param delay How long to delay (in milliseconds) when debouncing or
#' throttling, before sending the mouse location to the server.
#' @param delayType The type of algorithm for limiting the number of hover
#' events. Use `"throttle"` to limit the number of hover events to one
#' every `delay` milliseconds. Use `"debounce"` to suspend events
#' while the cursor is moving, and wait until the cursor has been at rest for
#' `delay` milliseconds before sending an event.
#' @param clip Should the hover area be clipped to the plotting area? If FALSE,
#' then the server will receive hover events even when the mouse is outside
#' the plotting area, as long as it is still inside the image.
#' @param nullOutside If `TRUE` (the default), the value will be set to
#' `NULL` when the mouse exits the plotting area. If `FALSE`, the
#' value will stop changing when the cursor exits the plotting area.
#' @export
#' @rdname clickOpts
hoverOpts <- function(id, delay = 300,
delayType = c("debounce", "throttle"), clip = TRUE,
nullOutside = TRUE) {
@@ -95,8 +75,12 @@ hoverOpts <- function(id, delay = 300,
#' `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 Fill color of the brush.
#' @param stroke Outline color of the brush.
#' @param fill Fill color of the brush. If `'auto'`, it derives from the link
#' color of the plot's HTML container (if **thematic** is enabled, and `accent`
#' is a non-`'auto'` value, that color is used instead).
#' @param stroke Outline color of the brush. If `'auto'`, it derives from the
#' foreground color of the plot's HTML container (if **thematic** is enabled,
#' and `fg` is a non-`'auto'` value, that color is used instead).
#' @param opacity Opacity of the brush
#' @param delay How long to delay (in milliseconds) when debouncing or
#' throttling, before sending the brush data to the server.
@@ -116,6 +100,7 @@ hoverOpts <- function(id, delay = 300,
#' `FALSE`, is useful if you want to update the plot while keeping the
#' brush. Using `TRUE` is useful if you want to clear the brush whenever
#' the plot is updated.
#' @seealso [clickOpts()] for clicking events.
#' @export
brushOpts <- function(id, fill = "#9cf", stroke = "#036",
opacity = 0.25, delay = 300,
@@ -125,6 +110,13 @@ brushOpts <- function(id, fill = "#9cf", stroke = "#036",
if (is.null(id))
stop("id must not be NULL")
if (identical(fill, "auto")) {
fill <- getThematicOption("accent", "auto")
}
if (identical(stroke, "auto")) {
stroke <- getThematicOption("fg", "auto")
}
list(
id = id,
fill = fill,
@@ -137,3 +129,13 @@ brushOpts <- function(id, fill = "#9cf", stroke = "#036",
resetOnNew = resetOnNew
)
}
getThematicOption <- function(name = "", default = NULL, resolve = FALSE) {
if (isNamespaceLoaded("thematic")) {
# TODO: use :: once thematic is on CRAN
tgo <- utils::getFromNamespace("thematic_get_option", "thematic")
tgo(name = name, default = default, resolve = resolve)
} else {
default
}
}

View File

@@ -92,11 +92,21 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
use_x <- grepl("x", brush$direction)
use_y <- grepl("y", brush$direction)
# We transitioned to using %||% in Shiny 1.6.0. Previously, these vars could
# be NA, because the old %OR% operator recognized NA. These warnings and
# the NULL replacement are here just to ease the transition in case anyone is
# using NA. We can remove these checks in a future version of Shiny.
# https://github.com/rstudio/shiny/pull/3172
if (is_na(xvar)) { xvar <- NULL; warning("xvar should be NULL, not NA.") }
if (is_na(yvar)) { yvar <- NULL; warning("yvar should be NULL, not NA.") }
if (is_na(panelvar1)) { panelvar1 <- NULL; warning("panelvar1 should be NULL, not NA.") }
if (is_na(panelvar2)) { panelvar2 <- NULL; warning("panelvar2 should be NULL, not NA.") }
# Try to extract vars from brush object
xvar <- xvar %OR% brush$mapping$x
yvar <- yvar %OR% brush$mapping$y
panelvar1 <- panelvar1 %OR% brush$mapping$panelvar1
panelvar2 <- panelvar2 %OR% brush$mapping$panelvar2
xvar <- xvar %||% brush$mapping$x
yvar <- yvar %||% brush$mapping$y
panelvar1 <- panelvar1 %||% brush$mapping$panelvar1
panelvar2 <- panelvar2 %||% brush$mapping$panelvar2
# Filter out x and y values
keep_rows <- rep(TRUE, nrow(df))
@@ -172,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
@@ -230,11 +240,21 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
stop("nearPoints requires a click/hover/double-click object with x and y values.")
}
# We transitioned to using %||% in Shiny 1.6.0. Previously, these vars could
# be NA, because the old %OR% operator recognized NA. These warnings and
# the NULL replacement are here just to ease the transition in case anyone is
# using NA. We can remove these checks in a future version of Shiny.
# https://github.com/rstudio/shiny/pull/3172
if (is_na(xvar)) { xvar <- NULL; warning("xvar should be NULL, not NA.") }
if (is_na(yvar)) { yvar <- NULL; warning("yvar should be NULL, not NA.") }
if (is_na(panelvar1)) { panelvar1 <- NULL; warning("panelvar1 should be NULL, not NA.") }
if (is_na(panelvar2)) { panelvar2 <- NULL; warning("panelvar2 should be NULL, not NA.") }
# Try to extract vars from coordinfo object
xvar <- xvar %OR% coordinfo$mapping$x
yvar <- yvar %OR% coordinfo$mapping$y
panelvar1 <- panelvar1 %OR% coordinfo$mapping$panelvar1
panelvar2 <- panelvar2 %OR% coordinfo$mapping$panelvar2
xvar <- xvar %||% coordinfo$mapping$x
yvar <- yvar %||% coordinfo$mapping$y
panelvar1 <- panelvar1 %||% coordinfo$mapping$panelvar1
panelvar2 <- panelvar2 %||% coordinfo$mapping$panelvar2
if (is.null(xvar))
stop("nearPoints: not able to automatically infer `xvar` from coordinfo")
@@ -247,6 +267,7 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
stop("nearPoints: `yvar` ('", yvar ,"') not in names of input")
# Extract data values from the data frame
coordinfo <- fortifyDiscreteLimits(coordinfo)
x <- asNumber(df[[xvar]], coordinfo$domain$discrete_limits$x)
y <- asNumber(df[[yvar]], coordinfo$domain$discrete_limits$y)
@@ -346,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"
@@ -372,6 +393,7 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
# an input brush
within_brush <- function(vals, brush, var = "x") {
var <- match.arg(var, c("x", "y"))
brush <- fortifyDiscreteLimits(brush)
vals <- asNumber(vals, brush$domain$discrete_limits[[var]])
# It's possible for a non-missing data values to not
# map to the axis limits, for example:
@@ -394,11 +416,43 @@ asNumber <- function(x, levels = NULL) {
as.numeric(x)
}
# Ensure the discrete limits/levels of a coordmap received
# from the client matches the data structure sent the client.
#
# When we construct the coordmap (in getGgplotCoordmap()),
# we save a character vector which may contain missing values
# (e.g., c("a", "b", NA)). When that same character is received
# from the client, it runs through decodeMessage() which sets
# simplifyVector=FALSE, which means NA are replaced by NULL
# (because jsonlite::fromJSON('["a", "b", null]') -> list("a", "b", NULL))
#
# Thankfully, it doesn't seem like it's meaningful for limits to
# contains a NULL in the 1st place, so we simply treat NULL like NA.
# For more context, https://github.com/rstudio/shiny/issues/2666
fortifyDiscreteLimits <- function(coord) {
# Note that discrete_limits$x/y are populated iff
# x/y are discrete mappings
coord$domain$discrete_limits <- lapply(
coord$domain$discrete_limits,
function(var) {
# if there is an 'explicit' NULL, then the limits are NA
if (is.null(var)) return(NA)
vapply(var, function(x) {
if (is.null(x) || isTRUE(is.na(x))) NA_character_ else x
}, character(1))
}
)
coord
}
# Given a panelvar value and a vector x, return logical vector indicating which
# items match the panelvar value. Because the panelvar value is always a
# string but the vector could be numeric, it might be necessary to coerce the
# panelvar to a number before comparing to the vector.
panelMatch <- function(search_value, x) {
if (is.null(search_value)) return(is.na(x))
if (is.numeric(x)) search_value <- as.numeric(search_value)
x == search_value
}

View File

@@ -1,30 +1,30 @@
startPNG <- function(filename, width, height, res, ...) {
# shiny.useragg is an experimental option that isn't officially supported or
# documented. It's here in the off chance that someone really wants
# to use ragg (say, instead of showtext, for custom font rendering).
# In the next shiny release, this option will likely be superseded in
# favor of a fully customizable graphics device option
if ((getOption('shiny.useragg') %OR% FALSE) && is_available("ragg")) {
pngfun <- ragg::agg_png
pngfun <- if ((getOption('shiny.useragg') %||% TRUE) && is_installed("ragg")) {
ragg::agg_png
} else if (capabilities("aqua")) {
# i.e., png(type = 'quartz')
pngfun <- grDevices::png
} else if ((getOption('shiny.usecairo') %OR% TRUE) && is_available("Cairo")) {
pngfun <- Cairo::CairoPNG
grDevices::png
} else if ((getOption('shiny.usecairo') %||% TRUE) && is_installed("Cairo")) {
Cairo::CairoPNG
} else {
# i.e., png(type = 'cairo')
pngfun <- grDevices::png
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
# devices allow their bg arg to be overridden by par(bg=...), which thematic does prior
# to plot-time, but it shouldn't hurt to inform other the device directly as well
if (is.null(args$bg) && isNamespaceLoaded("thematic")) {
# TODO: use :: once thematic is on CRAN
args$bg <- utils::getFromNamespace("thematic_get_option", "thematic")("bg", "white")
args$bg <- getThematicOption("bg", "white")
# auto vals aren't resolved until plot time, so if we see one, resolve it
if (isTRUE("auto" == args$bg)) {
args$bg <- getCurrentOutputInfo()[["bg"]]()
@@ -58,33 +58,35 @@ startPNG <- function(filename, width, height, res, ...) {
grDevices::dev.cur()
}
#' Run a plotting function and save the output as a PNG
#' Capture a plot as a PNG file.
#'
#' This function returns the name of the PNG file that it generates. In
#' essence, it calls `png()`, then `func()`, then `dev.off()`.
#' So `func` must be a function that will generate a plot when used this
#' way.
#' The PNG graphics device used is determined in the following order:
#' * If the ragg package is installed (and the `shiny.useragg` is not
#' set to `FALSE`), then use [ragg::agg_png()].
#' * If a quartz device is available (i.e., `capabilities("aqua")` is
#' `TRUE`), then use `png(type = "quartz")`.
#' * If the Cairo package is installed (and the `shiny.usecairo` option
#' is not set to `FALSE`), then use [Cairo::CairoPNG()].
#' * Otherwise, use [grDevices::png()]. In this case, Linux and Windows
#' may not antialias some point shapes, resulting in poor quality output.
#'
#' For output, it will try to use the following devices, in this order:
#' quartz (via [grDevices::png()]), then [Cairo::CairoPNG()],
#' and finally [grDevices::png()]. This is in order of quality of
#' output. Notably, plain `png` output on Linux and Windows may not
#' antialias some point shapes, resulting in poor quality output.
#'
#' In some cases, `Cairo()` provides output that looks worse than
#' `png()`. To disable Cairo output for an app, use
#' `options(shiny.usecairo=FALSE)`.
#' @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`.
#' @param width Width in pixels.
#' @param height Height in pixels.
#' @param res Resolution in pixels per inch. This value is passed to
#' [grDevices::png()]. Note that this affects the resolution of PNG rendering in
#' @param res Resolution in pixels per inch. This value is passed to the
#' graphics device. Note that this affects the resolution of PNG rendering in
#' R; it won't change the actual ppi of the browser.
#' @param ... Arguments to be passed through to [grDevices::png()].
#' These can be used to set the width, height, background color, etc.
#' @param ... Arguments to be passed through to the graphics device. These can
#' be used to set the width, height, background color, etc.
#'
#' @return A path to the newly generated PNG file.
#'
#' @export
plotPNG <- function(func, filename=tempfile(fileext='.png'),
width=400, height=400, res=72, ...) {
@@ -95,11 +97,10 @@ plotPNG <- function(func, filename=tempfile(fileext='.png'),
filename
}
#' @importFrom grDevices dev.set dev.cur
createGraphicsDevicePromiseDomain <- function(which = dev.cur()) {
force(which)
promises::new_promise_domain(
new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
function(...) {

View File

@@ -7,6 +7,8 @@
#' @param label The contents of the button or link--usually a text label, but
#' you could also use any other HTML, like an image.
#' @param icon An optional [icon()] to appear on the button.
#' @param disabled If `TRUE`, the button will not be clickable. Use
#' [updateActionButton()] to dynamically enable/disable the button.
#' @param ... Named attributes to be applied to the button or link.
#'
#' @family input elements
@@ -49,16 +51,29 @@
#' * Event handlers (e.g., [observeEvent()], [eventReactive()]) won't execute on initial load.
#' * Input validation (e.g., [req()], [need()]) will fail on initial load.
#' @export
actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
actionButton <- function(inputId, label, icon = NULL, width = NULL,
disabled = FALSE, ...) {
value <- restoreInput(id = inputId, default = NULL)
tags$button(id=inputId,
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
type="button",
class="btn btn-default action-button",
icon <- validateIcon(icon)
if (!is.null(icon)) {
icon <- span(icon, class = "action-icon")
}
if (!is.null(label)) {
label <- span(label, class = "action-label")
}
tags$button(
id = inputId,
style = css(width = validateCssUnit(width)),
type = "button",
class = "btn btn-default action-button",
`data-val` = value,
list(validateIcon(icon), label),
disabled = if (isTRUE(disabled)) NA else NULL,
icon, label,
...
)
}
@@ -68,30 +83,40 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
actionLink <- function(inputId, label, icon = NULL, ...) {
value <- restoreInput(id = inputId, default = NULL)
tags$a(id=inputId,
href="#",
class="action-button",
icon <- validateIcon(icon)
if (!is.null(icon)) {
icon <- span(icon, class = "action-icon")
}
if (!is.null(label)) {
label <- span(label, class = "action-label")
}
tags$a(
id = inputId,
href = "#",
class = "action-button action-link",
`data-val` = value,
list(validateIcon(icon), label),
icon, label,
...
)
}
# Check that the icon parameter is valid:
# 1) Check if the user wants to actually add an icon:
# -- if icon=NULL, it means leave the icon unchanged
# -- if icon=character(0), it means don't add an icon or, more usefully,
# remove the previous icon
# 2) If so, check that the icon has the right format (this does not check whether
# it is a *real* icon - currently that would require a massive cross reference
# with the "font-awesome" and the "glyphicon" libraries)
# Throw an informative warning if icon isn't html-ish
validateIcon <- function(icon) {
if (is.null(icon) || identical(icon, character(0))) {
if (length(icon) == 0) {
return(icon)
} else if (inherits(icon, "shiny.tag") && icon$name == "i") {
return(icon)
} else {
stop("Invalid icon. Use Shiny's 'icon()' function to generate a valid icon")
}
if (!isTagLike(icon)) {
rlang::warn(
c(
"It appears that a non-HTML value was provided to `icon`.",
i = "Try using a `shiny::icon()` (or an equivalent) to get an icon."
),
class = "shiny-validate-icon"
)
}
icon
}

View File

@@ -31,12 +31,12 @@ checkboxInput <- function(inputId, label, value = FALSE, width = NULL) {
value <- restoreInput(id = inputId, default = value)
inputTag <- tags$input(id = inputId, type="checkbox")
inputTag <- tags$input(id = inputId, type="checkbox", class = "shiny-input-checkbox")
if (!is.null(value) && value)
inputTag$attribs$checked <- "checked"
div(class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
style = css(width = validateCssUnit(width)),
div(class = "checkbox",
tags$label(inputTag, tags$span(label))
)

View File

@@ -94,10 +94,14 @@ checkboxGroupInput <- function(inputId, label, choices = NULL, selected = NULL,
divClass <- paste(divClass, "shiny-input-container-inline")
# return label and select tag
inputLabel <- shinyInputLabel(inputId, label)
tags$div(id = inputId,
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
style = css(width = validateCssUnit(width)),
class = divClass,
shinyInputLabel(inputId, label),
# https://www.w3.org/TR/wai-aria-practices/examples/checkbox/checkbox-1/checkbox-1.html
role = "group",
`aria-labelledby` = inputLabel$attribs$id,
inputLabel,
options
)
}

View File

@@ -105,7 +105,7 @@ dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
tags$div(id = inputId,
class = "shiny-date-input form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
tags$input(type = "text",
@@ -128,19 +128,53 @@ dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
`data-date-days-of-week-disabled` =
jsonlite::toJSON(daysofweekdisabled, null = 'null')
),
datePickerDependency
datePickerDependency()
)
}
datePickerDependency <- htmlDependency(
"bootstrap-datepicker", "1.6.4", c(href = "shared/datepicker"),
script = "js/bootstrap-datepicker.min.js",
stylesheet = "css/bootstrap-datepicker3.min.css",
# Need to enable noConflict mode. See #1346.
head = "<script>
(function() {
var datepicker = $.fn.datepicker.noConflict();
$.fn.bsDatepicker = datepicker;
})();
</script>"
)
datePickerDependency <- function(theme) {
list(
htmlDependency(
name = "bootstrap-datepicker-js",
version = version_bs_date_picker,
src = "www/shared/datepicker",
package = "shiny",
script = if (getOption("shiny.minified", TRUE)) "js/bootstrap-datepicker.min.js"
else "js/bootstrap-datepicker.js",
# Need to enable noConflict mode. See #1346.
head = "<script>(function() {
var datepicker = $.fn.datepicker.noConflict();
$.fn.bsDatepicker = datepicker;
})();
</script>"
),
bslib::bs_dependency_defer(datePickerCSS)
)
}
datePickerSass <- function() {
sass::sass_file(
system_file(package = "shiny", "www/shared/datepicker/scss/build3.scss")
)
}
datePickerCSS <- function(theme) {
if (!is_bs_theme(theme)) {
return(htmlDependency(
name = "bootstrap-datepicker-css",
version = version_bs_date_picker,
src = "www/shared/datepicker",
package = "shiny",
stylesheet = "css/bootstrap-datepicker3.min.css"
))
}
bslib::bs_dependency(
input = datePickerSass(),
theme = theme,
name = "bootstrap-datepicker",
version = version_bs_date_picker,
cache_key_extra = get_package_version("shiny")
)
}

View File

@@ -92,7 +92,7 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
attachDependencies(
div(id = inputId,
class = "shiny-date-range-input form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
# input-daterange class is needed for dropdown behavior
@@ -137,6 +137,6 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
)
)
),
datePickerDependency
datePickerDependency()
)
}

View File

@@ -2,8 +2,13 @@
#'
#' Create a file upload control that can be used to upload one or more files.
#'
#' Whenever a file upload completes, the corresponding input variable is set
#' to a dataframe. See the `Server value` section.
#' Whenever a file upload completes, the corresponding input variable is set to
#' a dataframe. See the `Server value` section.
#'
#' Each time files are uploaded, they are written to a new random subdirectory
#' inside of R's process-level temporary directory. The Shiny user session keeps
#' track of all uploads in the session, and when the session ends, Shiny deletes
#' all of the subdirectories where files where uploaded to.
#'
#' @family input elements
#'
@@ -11,19 +16,30 @@
#' @param multiple Whether the user should be allowed to select and upload
#' multiple files at once. **Does not work on older browsers, including
#' Internet Explorer 9 and earlier.**
#' @param accept A character vector of "unique file type specifiers" which
#' gives the browser a hint as to the type of file the server expects.
#' Many browsers use this prevent the user from selecting an invalid file.
#' @param accept A character vector of "unique file type specifiers" which gives
#' the browser a hint as to the type of file the server expects. Many browsers
#' use this prevent the user from selecting an invalid file.
#'
#' A unique file type specifier can be:
#' * A case insensitive extension like `.csv` or `.rds`.
#' * A valid MIME type, like `text/plain` or `application/pdf`
#' * One of `audio/*`, `video/*`, or `image/*` meaning any audio, video,
#' or image type, respectively.
#' or image type, respectively.
#' @param buttonLabel The label used on the button. Can be text or an HTML tag
#' object.
#' @param placeholder The text to show before a file has been uploaded.
#' @param capture What source to use for capturing image, audio or video data.
#' This attribute facilitates user access to a device's media capture
#' mechanism, such as a camera, or microphone, from within a file upload
#' control.
#'
#' A value of `user` indicates that the user-facing camera and/or microphone
#' should be used. A value of `environment` specifies that the outward-facing
#' camera and/or microphone should be used.
#'
#' By default on most phones, this will accept still photos or video. For
#' still photos only, also use `accept="image/*"`. For video only, use
#' `accept="video/*"`.
#' @examples
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
@@ -56,7 +72,9 @@
#' }
#'
#' @section Server value:
#' A `data.frame` that contains one row for each selected file, and following columns:
#'
#' A `data.frame` that contains one row for each selected file, and following
#' columns:
#' \describe{
#' \item{`name`}{The filename provided by the web browser. This is
#' **not** the path to read to get at the actual data that was uploaded
@@ -73,7 +91,8 @@
#'
#' @export
fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
width = NULL, buttonLabel = "Browse...", placeholder = "No file selected") {
width = NULL, buttonLabel = "Browse...", placeholder = "No file selected",
capture = NULL) {
restoredValue <- restoreInput(id = inputId, default = NULL)
@@ -89,6 +108,7 @@ fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
inputTag <- tags$input(
id = inputId,
class = "shiny-input-file",
name = inputId,
type = "file",
# Don't use "display: none;" style, which causes keyboard accessibility issue; instead use the following workaround: https://css-tricks.com/places-its-tempting-to-use-display-none-but-dont/
@@ -101,9 +121,12 @@ fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
if (length(accept) > 0)
inputTag$attribs$accept <- paste(accept, collapse=',')
if (!is.null(capture)) {
inputTag$attribs$capture <- capture
}
div(class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
div(class = "input-group",

View File

@@ -29,23 +29,37 @@
#' A numeric vector of length 1.
#'
#' @export
numericInput <- function(inputId, label, value, min = NA, max = NA, step = NA,
width = NULL) {
numericInput <- function(
inputId,
label,
value,
min = NA,
max = NA,
step = NA,
width = NULL,
...,
updateOn = c("change", "blur")
) {
rlang::check_dots_empty()
updateOn <- rlang::arg_match(updateOn)
value <- restoreInput(id = inputId, default = value)
# build input tag
inputTag <- tags$input(id = inputId, type = "number", class="form-control",
value = formatNoSci(value))
if (!is.na(min))
inputTag$attribs$min = min
if (!is.na(max))
inputTag$attribs$max = max
if (!is.na(step))
inputTag$attribs$step = step
inputTag <- tags$input(
id = inputId,
type = "number",
class = "shiny-input-number form-control",
value = formatNoSci(value),
`data-update-on` = updateOn
)
if (!is.na(min)) inputTag$attribs$min = min
if (!is.na(max)) inputTag$attribs$max = max
if (!is.na(step)) inputTag$attribs$step = step
div(class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
div(
class = "form-group shiny-input-container",
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
inputTag
)

View File

@@ -30,12 +30,29 @@
#' shinyApp(ui, server)
#' }
#' @export
passwordInput <- function(inputId, label, value = "", width = NULL,
placeholder = NULL) {
div(class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
passwordInput <- function(
inputId,
label,
value = "",
width = NULL,
placeholder = NULL,
...,
updateOn = c("change", "blur")
) {
rlang::check_dots_empty()
updateOn <- rlang::arg_match(updateOn)
div(
class = "form-group shiny-input-container",
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
tags$input(id = inputId, type="password", class="form-control", value=value,
placeholder = placeholder)
tags$input(
id = inputId,
type = "password",
class = "shiny-input-password form-control",
value = value,
placeholder = placeholder,
`data-update-on` = updateOn
)
)
}

View File

@@ -11,22 +11,22 @@
#' @inheritParams textInput
#' @param choices List of values to select from (if elements of the list are
#' named then that name rather than the value is displayed to the user). If
#' this argument is provided, then `choiceNames` and `choiceValues`
#' must not be provided, and vice-versa. The values should be strings; other
#' types (such as logicals and numbers) will be coerced to strings.
#' @param selected The initially selected value (if not specified then defaults
#' to the first value)
#' this argument is provided, then `choiceNames` and `choiceValues` must not
#' be provided, and vice-versa. The values should be strings; other types
#' (such as logicals and numbers) will be coerced to strings.
#' @param selected The initially selected value. If not specified, then it
#' defaults to the first item in `choices`. To start with no items selected,
#' use `character(0)`.
#' @param inline If `TRUE`, render the choices inline (i.e. horizontally)
#' @return A set of radio buttons that can be added to a UI definition.
#' @param choiceNames,choiceValues List of names and values, respectively, that
#' are displayed to the user in the app and correspond to the each choice (for
#' this reason, `choiceNames` and `choiceValues` must have the same
#' length). If either of these arguments is provided, then the other
#' *must* be provided and `choices` *must not* be provided. The
#' advantage of using both of these over a named list for `choices` is
#' that `choiceNames` allows any type of UI object to be passed through
#' (tag objects, icons, HTML code, ...), instead of just simple text. See
#' Examples.
#' this reason, `choiceNames` and `choiceValues` must have the same length).
#' If either of these arguments is provided, then the other *must* be provided
#' and `choices` *must not* be provided. The advantage of using both of these
#' over a named list for `choices` is that `choiceNames` allows any type of UI
#' object to be passed through (tag objects, icons, HTML code, ...), instead
#' of just simple text. See Examples.
#'
#' @family input elements
#' @seealso [updateRadioButtons()]
@@ -82,7 +82,8 @@
#' }
#'
#' @section Server value:
#' A character string containing the value of the selected button.
#'
#' A character string containing the value of the selected button.
#'
#' @export
radioButtons <- function(inputId, label, choices = NULL, selected = NULL,
@@ -103,10 +104,14 @@ radioButtons <- function(inputId, label, choices = NULL, selected = NULL,
divClass <- "form-group shiny-input-radiogroup shiny-input-container"
if (inline) divClass <- paste(divClass, "shiny-input-container-inline")
inputLabel <- shinyInputLabel(inputId, label)
tags$div(id = inputId,
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
style = css(width = validateCssUnit(width)),
class = divClass,
shinyInputLabel(inputId, label),
# https://www.w3.org/TR/2017/WD-wai-aria-practices-1.1-20170628/examples/radio/radio-1/radio-1.html
role = "radiogroup",
`aria-labelledby` = inputLabel$attribs$id,
inputLabel,
options
)
}

View File

@@ -4,7 +4,7 @@
#' from a list of values.
#'
#' By default, `selectInput()` and `selectizeInput()` use the JavaScript library
#' \pkg{selectize.js} (<https://github.com/selectize/selectize.js>) instead of
#' \pkg{selectize.js} (<https://selectize.dev/>) instead of
#' the basic select input element. To use the standard HTML select input
#' element, use `selectInput()` with `selectize=FALSE`.
#'
@@ -12,6 +12,14 @@
#' name will be treated as a placeholder prompt. For example:
#' `selectInput("letter", "Letter", c("Choose one" = "", LETTERS))`
#'
#' **Performance note:** `selectInput()` and `selectizeInput()` can slow down
#' significantly when thousands of choices are used; with legacy browsers like
#' Internet Explorer, the user interface may hang for many seconds. For large
#' numbers of choices, Shiny offers a "server-side selectize" option that
#' massively improves performance and efficiency; see
#' [this selectize article](https://shiny.rstudio.com/articles/selectize.html)
#' on the Shiny Dev Center for details.
#'
#' @inheritParams textInput
#' @param choices List of values to select from. If elements of the list are
#' named, then that name --- rather than the value --- is displayed to the
@@ -98,9 +106,10 @@ selectInput <- function(inputId, label, choices, selected = NULL,
# create select tag and add options
selectTag <- tags$select(
id = inputId,
class = "shiny-input-select",
class = if (!selectize) "form-control",
size = size,
selectOptions(choices, selected)
selectOptions(choices, selected, inputId, selectize)
)
if (multiple)
selectTag$attribs$multiple <- "multiple"
@@ -108,7 +117,7 @@ selectInput <- function(inputId, label, choices, selected = NULL,
# return label and select tag
res <- div(
class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
div(selectTag)
)
@@ -125,16 +134,22 @@ firstChoice <- function(choices) {
}
# Create tags for each of the options; use <optgroup> if necessary.
# This returns a HTML string instead of tags, because of the 'selected'
# attribute.
selectOptions <- function(choices, selected = NULL) {
# This returns a HTML string instead of tags for performance reasons.
selectOptions <- function(choices, selected = NULL, inputId, perfWarning = FALSE) {
if (length(choices) >= 1000) {
warning("The select input \"", inputId, "\" contains a large number of ",
"options; consider using server-side selectize for massively improved ",
"performance. See the Details section of the ?selectizeInput help topic.",
call. = FALSE)
}
html <- mapply(choices, names(choices), FUN = function(choice, label) {
if (is.list(choice)) {
# If sub-list, create an optgroup and recurse into the sublist
sprintf(
'<optgroup label="%s">\n%s\n</optgroup>',
htmlEscape(label, TRUE),
selectOptions(choice, selected)
selectOptions(choice, selected, inputId, perfWarning)
)
} else {
@@ -158,7 +173,7 @@ needOptgroup <- function(choices) {
#' @rdname selectInput
#' @param ... Arguments passed to `selectInput()`.
#' @param options A list of options. See the documentation of \pkg{selectize.js}
#' @param options A list of options. See the documentation of \pkg{selectize.js}(<https://selectize.dev/docs/usage>)
#' for possible options (character option values inside [base::I()] will
#' be treated as literal JavaScript code; see [renderDataTable()]
#' for details).
@@ -183,6 +198,12 @@ selectizeInput <- function(inputId, ..., options = NULL, width = NULL) {
# given a select input and its id, selectize it
selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
if (length(options) == 0) {
# For NULL and empty unnamed list, replace with an empty named list, so that
# it will get translated to {} in JSON later on.
options <- empty_named_list()
}
# Make sure accessibility plugin is included
if (!('selectize-plugin-a11y' %in% options$plugins)) {
options$plugins <- c(options$plugins, list('selectize-plugin-a11y'))
@@ -190,21 +211,10 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
res <- checkAsIs(options)
selectizeDep <- htmlDependency(
"selectize", "0.12.4", c(href = "shared/selectize"),
stylesheet = "css/selectize.bootstrap3.css",
head = format(tagList(
tags$script(src = 'shared/selectize/js/selectize.min.js'),
# Accessibility plugin for screen readers (https://github.com/SLMNBJ/selectize-plugin-a11y):
tags$script(src = 'shared/selectize/accessibility/js/selectize-plugin-a11y.min.js')
))
)
deps <- list(selectizeDependency())
if ('drag_drop' %in% options$plugins) {
selectizeDep <- list(selectizeDep, htmlDependency(
'jqueryui', '1.12.1', c(href = 'shared/jqueryui'),
script = 'jquery-ui.min.js'
))
deps[[length(deps) + 1]] <- jqueryuiDependency()
}
# Insert script on same level as <select> tag
@@ -214,18 +224,74 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
type = 'application/json',
`data-for` = inputId, `data-nonempty` = if (nonempty) '',
`data-eval` = if (length(res$eval)) HTML(toJSON(res$eval)),
if (length(res$options)) HTML(toJSON(res$options)) else '{}'
HTML(toJSON(res$options))
)
)
attachDependencies(select, selectizeDep)
attachDependencies(select, deps)
}
selectizeDependency <- function() {
bslib::bs_dependency_defer(selectizeDependencyFunc)
}
selectizeDependencyFunc <- function(theme) {
if (!is_bs_theme(theme)) {
return(selectizeStaticDependency(version_selectize))
}
bs_version <- bslib::theme_version(theme)
# It'd be cleaner to ship the JS in a separate, href-based,
# HTML dependency (which we currently do for other themable widgets),
# but DT, crosstalk, and maybe other pkgs include selectize JS/CSS
# in HTML dependency named selectize, so if we were to change that
# name, the JS/CSS would be loaded/included twice, which leads to
# strange issues, especially since we now include a 3rd party
# accessibility plugin https://github.com/rstudio/shiny/pull/3153
selectizeDir <- system_file(package = "shiny", "www/shared/selectize/")
script <- file.path(selectizeDir, selectizeScripts())
bslib::bs_dependency(
input = selectizeSass(bs_version),
theme = theme,
name = "selectize",
version = version_selectize,
cache_key_extra = get_package_version("shiny"),
.dep_args = list(script = script)
)
}
selectizeSass <- function(bs_version) {
selectizeDir <- system_file(package = "shiny", "www/shared/selectize/")
stylesheet <- file.path(
selectizeDir, "scss", paste0("selectize.bootstrap", bs_version, ".scss")
)
sass::sass_file(stylesheet)
}
selectizeStaticDependency <- function(version) {
htmlDependency(
"selectize",
version,
src = "www/shared/selectize",
package = "shiny",
stylesheet = "css/selectize.bootstrap3.css",
script = selectizeScripts()
)
}
selectizeScripts <- function() {
isMinified <- isTRUE(get_devmode_option("shiny.minified", TRUE))
paste0(
c(
"js/selectize",
"accessibility/js/selectize-plugin-a11y"
),
if (isMinified) ".min.js" else ".js"
)
}
#' Select variables from a data frame
@@ -235,7 +301,7 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
#'
#' By default, `varSelectInput()` and `selectizeInput()` use the
#' JavaScript library \pkg{selectize.js}
#' (<https://github.com/selectize/selectize.js>) to instead of the basic
#' (<https://selectize.dev/>) to instead of the basic
#' select input element. To use the standard HTML select input element, use
#' `selectInput()` with `selectize=FALSE`.
#'
@@ -331,7 +397,7 @@ varSelectInput <- function(
#' @rdname varSelectInput
#' @param ... Arguments passed to `varSelectInput()`.
#' @param options A list of options. See the documentation of \pkg{selectize.js}
#' @param options A list of options. See the documentation of \pkg{selectize.js}(<https://selectize.dev/docs/usage>)
#' for possible options (character option values inside [base::I()] will
#' be treated as literal JavaScript code; see [renderDataTable()]
#' for details).

View File

@@ -1,25 +1,24 @@
#' Slider Input Widget
#'
#' Constructs a slider widget to select a numeric value from a range.
#' Constructs a slider widget to select a number, date, or date-time from a
#' range.
#'
#' @inheritParams textInput
#' @param min The minimum value (inclusive) that can be selected.
#' @param max The maximum value (inclusive) that can be selected.
#' @param value The initial value of the slider. A numeric vector of length one
#' will create a regular slider; a numeric vector of length two will create a
#' double-ended range slider. A warning will be issued if the value doesn't
#' fit between `min` and `max`.
#' @param min,max The minimum and maximum values (inclusive) that can be
#' selected.
#' @param value The initial value of the slider, either a number, a date
#' (class Date), or a date-time (class POSIXt). A length one vector will
#' create a regular slider; a length two vector will create a double-ended
#' range slider. Must lie between `min` and `max`.
#' @param step Specifies the interval between each selectable value on the
#' slider (if `NULL`, a heuristic is used to determine the step size). If
#' the values are dates, `step` is in days; if the values are times
#' (POSIXt), `step` is in seconds.
#' slider. Either `NULL`, the default, which uses a heuristic to determine the
#' step size or a single number. If the values are dates, `step` is in days;
#' if the values are date-times, `step` is in seconds.
#' @param round `TRUE` to round all values to the nearest integer;
#' `FALSE` if no rounding is desired; or an integer to round to that
#' number of digits (for example, 1 will round to the nearest 10, and -2 will
#' round to the nearest .01). Any rounding will be applied after snapping to
#' the nearest step.
#' @param format Deprecated.
#' @param locale Deprecated.
#' @param ticks `FALSE` to hide tick marks, `TRUE` to show them
#' according to some simple heuristics.
#' @param animate `TRUE` to show simple animation controls with default
@@ -72,23 +71,15 @@
#' }
#'
#' @section Server value:
#' A number, or in the case of slider range, a vector of two numbers.
#' A number, date, or date-time (depending on the class of `value`), or
#' in the case of slider range, a vector of two numbers/dates/date-times.
#'
#' @export
sliderInput <- function(inputId, label, min, max, value, step = NULL,
round = FALSE, format = NULL, locale = NULL,
ticks = TRUE, animate = FALSE, width = NULL, sep = ",",
pre = NULL, post = NULL, timeFormat = NULL,
timezone = NULL, dragRange = TRUE)
{
if (!missing(format)) {
shinyDeprecated(msg = "The `format` argument to sliderInput is deprecated. Use `sep`, `pre`, and `post` instead.",
version = "0.10.2.2")
}
if (!missing(locale)) {
shinyDeprecated(msg = "The `locale` argument to sliderInput is deprecated. Use `sep`, `pre`, and `post` instead.",
version = "0.10.2.2")
}
round = FALSE, ticks = TRUE, animate = FALSE,
width = NULL, sep = ",", pre = NULL, post = NULL,
timeFormat = NULL, timezone = NULL, dragRange = TRUE) {
validate_slider_value(min, max, value, "sliderInput")
dataType <- getSliderType(min, max, value)
@@ -144,6 +135,7 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
sliderProps <- dropNulls(list(
class = "js-range-slider",
id = inputId,
`data-skin` = "shiny",
`data-type` = if (length(value) > 1) "double",
`data-min` = formatNoSci(min),
`data-max` = formatNoSci(max),
@@ -175,7 +167,7 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
})
sliderTag <- div(class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
do.call(tags$input, sliderProps)
)
@@ -205,20 +197,58 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
)
}
dep <- list(
htmlDependency("ionrangeslider", "2.1.6", c(href="shared/ionrangeslider"),
script = "js/ion.rangeSlider.min.js",
# ion.rangeSlider also needs normalize.css, which is already included in
# Bootstrap.
stylesheet = c("css/ion.rangeSlider.css",
"css/ion.rangeSlider.skinShiny.css")
attachDependencies(sliderTag, ionRangeSliderDependency())
}
ionRangeSliderDependency <- function() {
list(
# ion.rangeSlider also needs normalize.css, which is already included in Bootstrap.
htmlDependency(
"ionrangeslider-javascript",
version_ion_range_slider,
src = "www/shared/ionrangeslider",
package = "shiny",
script = "js/ion.rangeSlider.min.js"
),
htmlDependency("strftime", "0.9.2", c(href="shared/strftime"),
htmlDependency(
"strftime",
version_strftime,
src = "www/shared/strftime",
package = "shiny",
script = "strftime-min.js"
),
bslib::bs_dependency_defer(ionRangeSliderDependencyCSS)
)
}
ionRangeSliderDependencySass <- function() {
list(
list(accent = "$component-active-bg"),
sass::sass_file(
system_file(package = "shiny", "www/shared/ionrangeslider/scss/shiny.scss")
)
)
}
attachDependencies(sliderTag, dep)
ionRangeSliderDependencyCSS <- function(theme) {
if (!is_bs_theme(theme)) {
return(htmlDependency(
"ionrangeslider-css",
version_ion_range_slider,
src = "www/shared/ionrangeslider",
package = "shiny",
stylesheet = "css/ion.rangeSlider.css"
))
}
bslib::bs_dependency(
input = ionRangeSliderDependencySass(),
theme = theme,
name = "ionRangeSlider",
version = version_ion_range_slider,
cache_key_extra = get_package_version("shiny")
)
}
hasDecimals <- function(value) {
@@ -226,7 +256,6 @@ hasDecimals <- function(value) {
return (!identical(value, truncatedValue))
}
# If step is NULL, use heuristic to set the step size.
findStepSize <- function(min, max, step) {
if (!is.null(step)) return(step)
@@ -253,6 +282,37 @@ findStepSize <- function(min, max, step) {
}
}
# Throw a warning if ever `value` is not in the [`min`, `max`] range
validate_slider_value <- function(min, max, value, fun) {
if (length(min) != 1 || is_na(min) ||
length(max) != 1 || is_na(max) ||
length(value) < 1 || length(value) > 2 || any(is.na(value)))
{
stop(call. = FALSE,
sprintf("In %s(): `min`, `max`, and `value` cannot be NULL, NA, or empty.", fun)
)
}
if (min(value) < min) {
warning(call. = FALSE,
sprintf(
"In %s(): `value` should be greater than or equal to `min` (value = %s, min = %s).",
fun, paste(value, collapse = ", "), min
)
)
}
if (max(value) > max) {
warning(
noBreaks. = TRUE, call. = FALSE,
sprintf(
"In %s(): `value` should be less than or equal to `max` (value = %s, max = %s).",
fun, paste(value, collapse = ", "), max
)
)
}
}
#' @rdname sliderInput
#'

View File

@@ -10,7 +10,7 @@
#' [actionButton()] instead of `submitButton` when you
#' want to delay a reaction.
#' See [this
#' article](http://shiny.rstudio.com/articles/action-buttons.html) for more information (including a demo of how to "translate"
#' article](https://shiny.rstudio.com/articles/action-buttons.html) for more information (including a demo of how to "translate"
#' code using a `submitButton` to code using an `actionButton`).
#'
#' In essence, the presence of a submit button stops all inputs from
@@ -57,8 +57,8 @@ submitButton <- function(text = "Apply Changes", icon = NULL, width = NULL) {
div(
tags$button(
type="submit",
class="btn btn-primary",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
class="btn btn-primary shiny-submit-button",
style = css(width = validateCssUnit(width)),
list(icon, text)
)
)

View File

@@ -10,6 +10,14 @@
#' @param placeholder A character string giving the user a hint as to what can
#' be entered into the control. Internet Explorer 8 and 9 do not support this
#' option.
#' @param ... Ignored, included to require named arguments and for future
#' feature expansion.
#' @param updateOn A character vector specifying when the input should be
#' updated. Options are `"change"` (default) and `"blur"`. Use `"change"` to
#' update the input immediately whenever the value changes. Use `"blur"`to
#' delay the input update until the input loses focus (the user moves away
#' from the input), or when Enter is pressed (or Cmd/Ctrl + Enter for
#' [textAreaInput()]).
#' @return A text input control that can be added to a UI definition.
#'
#' @family input elements
@@ -34,15 +42,31 @@
#' unless `value` is provided.
#'
#' @export
textInput <- function(inputId, label, value = "", width = NULL,
placeholder = NULL) {
textInput <- function(
inputId,
label,
value = "",
width = NULL,
placeholder = NULL,
...,
updateOn = c("change", "blur")
) {
rlang::check_dots_empty()
updateOn <- rlang::arg_match(updateOn)
value <- restoreInput(id = inputId, default = value)
div(class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
div(
class = "form-group shiny-input-container",
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
tags$input(id = inputId, type="text", class="form-control", value=value,
placeholder = placeholder)
tags$input(
id = inputId,
type = "text",
class = "shiny-input-text form-control",
value = value,
placeholder = placeholder,
`data-update-on` = updateOn
)
)
}

View File

@@ -16,6 +16,8 @@
#' @param resize Which directions the textarea box can be resized. Can be one of
#' `"both"`, `"none"`, `"vertical"`, and `"horizontal"`. The default, `NULL`,
#' will use the client browser's default setting for resizing textareas.
#' @param autoresize If `TRUE`, the textarea will automatically resize to fit
#' the input text.
#' @return A textarea input control that can be added to a UI definition.
#'
#' @family input elements
@@ -41,8 +43,22 @@
#' unless `value` is provided.
#'
#' @export
textAreaInput <- function(inputId, label, value = "", width = NULL, height = NULL,
cols = NULL, rows = NULL, placeholder = NULL, resize = NULL) {
textAreaInput <- function(
inputId,
label,
value = "",
width = NULL,
height = NULL,
cols = NULL,
rows = NULL,
placeholder = NULL,
resize = NULL,
...,
autoresize = FALSE,
updateOn = c("change", "blur")
) {
rlang::check_dots_empty()
updateOn <- rlang::arg_match(updateOn)
value <- restoreInput(id = inputId, default = value)
@@ -50,25 +66,30 @@ textAreaInput <- function(inputId, label, value = "", width = NULL, height = NUL
resize <- match.arg(resize, c("both", "none", "vertical", "horizontal"))
}
style <- paste(
if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
if (!is.null(height)) paste0("height: ", validateCssUnit(height), ";"),
if (!is.null(resize)) paste0("resize: ", resize, ";")
)
classes <- "form-control"
if (autoresize) {
classes <- c(classes, "textarea-autoresize")
if (is.null(rows)) {
rows <- 1
}
}
# Workaround for tag attribute=character(0) bug:
# https://github.com/rstudio/htmltools/issues/65
if (length(style) == 0) style <- NULL
div(class = "form-group shiny-input-container",
div(
class = "shiny-input-textarea form-group shiny-input-container",
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
tags$textarea(
id = inputId,
class = "form-control",
class = classes,
placeholder = placeholder,
style = style,
style = css(
width = if (!is.null(width)) "100%",
height = validateCssUnit(height),
resize = resize
),
rows = rows,
cols = cols,
`data-update-on` = updateOn,
value
)
)

View File

@@ -41,7 +41,7 @@ normalizeChoicesArgs <- function(choices, choiceNames, choiceValues,
if (length(choiceNames) != length(choiceValues)) {
stop("`choiceNames` and `choiceValues` must have the same length.")
}
if (anyNamed(choiceNames) || anyNamed(choiceValues)) {
if (any_named(choiceNames) || any_named(choiceValues)) {
stop("`choiceNames` and `choiceValues` must not be named.")
}
} else {

View File

@@ -112,35 +112,13 @@
#'
#' }
#' @export
insertTab <- function(inputId, tab, target,
position = c("before", "after"), select = FALSE,
insertTab <- function(inputId, tab, target = NULL,
position = c("after", "before"), select = FALSE,
session = getDefaultReactiveDomain()) {
force(target)
force(select)
position <- match.arg(position)
inputId <- session$ns(inputId)
# Barbara -- August 2017
# Note: until now, the number of tabs in a tabsetPanel (or navbarPage
# or navlistPanel) was always fixed. So, an easy way to give an id to
# a tab was simply incrementing a counter. (Just like it was easy to
# give a random 4-digit number to identify the tabsetPanel). Since we
# can only know this in the client side, we'll just pass `id` and
# `tsid` (TabSetID) as dummy values that will be fixed in the JS code.
item <- buildTabItem("id", "tsid", TRUE, divTag = tab,
textFilter = if (is.character(tab)) navbarMenuTextFilter else NULL)
callback <- function() {
session$sendInsertTab(
inputId = inputId,
liTag = processDeps(item$liTag, session),
divTag = processDeps(item$divTag, session),
menuName = NULL,
target = target,
position = position,
select = select)
}
session$onFlush(callback, once = TRUE)
bslib::nav_insert(
inputId, tab, target,
match.arg(position), select, session
)
}
#' @param menuName This argument should only be used when you want to
@@ -159,63 +137,21 @@ insertTab <- function(inputId, tab, target,
#' @export
prependTab <- function(inputId, tab, select = FALSE, menuName = NULL,
session = getDefaultReactiveDomain()) {
force(select)
force(menuName)
inputId <- session$ns(inputId)
item <- buildTabItem("id", "tsid", TRUE, divTag = tab,
textFilter = if (is.character(tab)) navbarMenuTextFilter else NULL)
callback <- function() {
session$sendInsertTab(
inputId = inputId,
liTag = processDeps(item$liTag, session),
divTag = processDeps(item$divTag, session),
menuName = menuName,
target = NULL,
position = "after",
select = select)
}
session$onFlush(callback, once = TRUE)
bslib::nav_prepend(inputId, tab, menu_title = menuName, select = select, session = session)
}
#' @rdname insertTab
#' @export
appendTab <- function(inputId, tab, select = FALSE, menuName = NULL,
session = getDefaultReactiveDomain()) {
force(select)
force(menuName)
inputId <- session$ns(inputId)
item <- buildTabItem("id", "tsid", TRUE, divTag = tab,
textFilter = if (is.character(tab)) navbarMenuTextFilter else NULL)
callback <- function() {
session$sendInsertTab(
inputId = inputId,
liTag = processDeps(item$liTag, session),
divTag = processDeps(item$divTag, session),
menuName = menuName,
target = NULL,
position = "before",
select = select)
}
session$onFlush(callback, once = TRUE)
bslib::nav_append(inputId, tab, menu_title = menuName, select = select, session = session)
}
#' @rdname insertTab
#' @export
removeTab <- function(inputId, target,
session = getDefaultReactiveDomain()) {
force(target)
inputId <- session$ns(inputId)
callback <- function() {
session$sendRemoveTab(
inputId = inputId,
target = target)
}
session$onFlush(callback, once = TRUE)
bslib::nav_remove(inputId, target, session)
}

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

@@ -76,16 +76,20 @@ absolutePanel <- function(...,
style <- paste(paste(names(cssProps), cssProps, sep = ':', collapse = ';'), ';', sep='')
divTag <- tags$div(style=style, ...)
if (isTRUE(draggable)) {
divTag <- tagAppendAttributes(divTag, class='draggable')
return(tagList(
singleton(tags$head(tags$script(src='shared/jqueryui/jquery-ui.min.js'))),
divTag,
tags$script('$(".draggable").draggable();')
))
} else {
if (identical(draggable, FALSE)) {
return(divTag)
}
# Add Shiny inputs and htmlwidgets to 'non-draggable' elements
# Cf. https://api.jqueryui.com/draggable/#option-cancel
dragOpts <- '{cancel: ".shiny-input-container,.html-widget,input,textarea,button,select,option"}'
dragJS <- sprintf('$(".draggable").draggable(%s);', dragOpts)
tagList(
tagAppendAttributes(divTag, class='draggable'),
jqueryuiDependency(),
tags$script(HTML(dragJS))
)
}
#' @rdname absolutePanel
@@ -99,3 +103,14 @@ fixedPanel <- function(...,
width=width, height=height, draggable=draggable, cursor=match.arg(cursor),
fixed=TRUE)
}
jqueryuiDependency <- function() {
htmlDependency(
"jqueryui",
version_jqueryui,
src = "www/shared/jqueryui",
package = "shiny",
script = "jquery-ui.min.js"
)
}

81
R/knitr.R Normal file
View File

@@ -0,0 +1,81 @@
#' Knitr S3 methods
#'
#' These S3 methods are necessary to help Shiny applications and UI chunks embed
#' themselves in knitr/rmarkdown documents.
#'
#' @name knitr_methods
#' @keywords internal
#' @param x Object to knit_print
#' @param ... Additional knit_print arguments
NULL
# If there's an R Markdown runtime option set but it isn't set to Shiny, then
# return a warning indicating the runtime is inappropriate for this object.
# Returns NULL in all other cases.
shiny_rmd_warning <- function() {
runtime <- knitr::opts_knit$get("rmarkdown.runtime")
if (!is.null(runtime) && runtime != "shiny")
# note that the RStudio IDE checks for this specific string to detect Shiny
# applications in static document
list(structure(
"Shiny application in a static R Markdown document",
class = "rmd_warning"))
else
NULL
}
#' @rdname knitr_methods
knit_print.shiny.appobj <- function(x, ...) {
opts <- x$options %||% list()
width <- if (is.null(opts$width)) "100%" else opts$width
height <- if (is.null(opts$height)) "400" else opts$height
runtime <- knitr::opts_knit$get("rmarkdown.runtime")
if (!is.null(runtime) && runtime != "shiny") {
# If not rendering to a Shiny document, create a box exactly the same
# dimensions as the Shiny app would have had (so the document continues to
# flow as it would have with the app), and display a diagnostic message
width <- validateCssUnit(width)
height <- validateCssUnit(height)
output <- tags$div(
style=paste("width:", width, "; height:", height, "; text-align: center;",
"box-sizing: border-box;", "-moz-box-sizing: border-box;",
"-webkit-box-sizing: border-box;"),
class="muted well",
"Shiny applications not supported in static R Markdown documents")
}
else {
path <- addSubApp(x)
output <- deferredIFrame(path, width, height)
}
# If embedded Shiny apps ever have JS/CSS dependencies (like pym.js) we'll
# need to grab those and put them in meta, like in knit_print.shiny.tag. But
# for now it's not an issue, so just return the HTML and warning.
knitr::asis_output(htmlPreserve(format(output, indent=FALSE)),
meta = shiny_rmd_warning(), cacheable = FALSE)
}
# Let us use a nicer syntax in knitr chunks than literally
# calling output$value <- renderFoo(...) and fooOutput().
#' @rdname knitr_methods
#' @param inline Whether the object is printed inline.
knit_print.shiny.render.function <- function(x, ..., inline = FALSE) {
x <- htmltools::as.tags(x, inline = inline)
output <- knitr::knit_print(tagList(x), ..., inline = inline)
attr(output, "knit_cacheable") <- FALSE
attr(output, "knit_meta") <- append(attr(output, "knit_meta"),
shiny_rmd_warning())
output
}
# Lets us drop reactive expressions directly into a knitr chunk and have the
# value printed out! Nice for teaching if nothing else.
#' @rdname knitr_methods
knit_print.reactive <- function(x, ..., inline = FALSE) {
renderFunc <- if (inline) renderText else renderPrint
knitr::knit_print(renderFunc({
x()
}), ..., inline = inline)
}

27
R/map.R
View File

@@ -1,19 +1,3 @@
# TESTS
# Simple set/get
# Simple remove
# Simple containsKey
# Simple keys
# Simple values
# Simple clear
# Get of unknown key returns NULL
# Remove of unknown key does nothing
# Setting a key twice always results in last-one-wins
# /TESTS
# Note that Map objects can't be saved in one R session and restored in
# another, because they are based on fastmap, which uses an external pointer,
# and external pointers can't be saved and restored in another session.
#' @importFrom fastmap fastmap
Map <- R6Class(
'Map',
portable = FALSE,
@@ -64,9 +48,12 @@ Map <- R6Class(
)
)
as.list.Map <- function(map) {
map$values()
#' @export
as.list.Map <- function(x, ...) {
x$values()
}
length.Map <- function(map) {
map$size()
#' @export
length.Map <- function(x) {
x$size()
}

View File

@@ -309,7 +309,7 @@ HandlerManager <- R6Class("HandlerManager",
createHttpuvApp = function() {
list(
onHeaders = function(req) {
maxSize <- getOption('shiny.maxRequestSize') %OR% (5 * 1024 * 1024)
maxSize <- getOption('shiny.maxRequestSize') %||% (5 * 1024 * 1024)
if (maxSize <= 0)
return(NULL)
@@ -346,9 +346,9 @@ HandlerManager <- R6Class("HandlerManager",
),
catch = function(err) {
httpResponse(status = 500L,
content_type = "text/html",
content_type = "text/html; charset=UTF-8",
content = as.character(htmltools::htmlTemplate(
system.file("template", "error.html", package = "shiny"),
system_file("template", "error.html", package = "shiny"),
message = conditionMessage(err)
))
)
@@ -426,7 +426,7 @@ HandlerManager <- R6Class("HandlerManager",
)
maybeInjectAutoreload <- function(resp) {
if (getOption("shiny.autoreload", FALSE) &&
if (get_devmode_option("shiny.autoreload", FALSE) &&
isTRUE(grepl("^text/html($|;)", resp$content_type)) &&
is.character(resp$content)) {

View File

@@ -1,5 +1,5 @@
# Promise helpers taken from:
# https://github.com/rstudio/promises/blob/master/tests/testthat/common.R
# https://github.com/rstudio/promises/blob/main/tests/testthat/common.R
# Block until all pending later tasks have executed
wait_for_it <- function() {
while (!later::loop_empty()) {
@@ -9,8 +9,6 @@ wait_for_it <- function() {
# Block until the promise is resolved/rejected. If resolved, return the value.
# If rejected, throw (yes throw, not return) the error.
#' @importFrom promises %...!%
#' @importFrom promises %...>%
extract <- function(promise) {
promise_value <- NULL
error <- NULL
@@ -31,7 +29,6 @@ extract <- function(promise) {
#' @noRd
#' @export
`$.mockclientdata` <- function(x, name) {
if (name == "allowDataUriScheme") { return(TRUE) }
if (name == "pixelratio") { return(1) }
if (name == "url_protocol") { return("http:") }
if (name == "url_hostname") { return("mocksession") }
@@ -156,6 +153,8 @@ makeExtraMethods <- function() {
"sendInsertTab",
"sendInsertUI",
"sendModal",
"setCurrentTheme",
"getCurrentTheme",
"sendNotification",
"sendProgress",
"sendRemoveTab",
@@ -167,12 +166,10 @@ makeExtraMethods <- function() {
), makeErrors(
`@uploadEnd` = "for internal use only",
`@uploadInit` = "for internal use only",
`@uploadieFinish` = "for internal use only",
createBookmarkObservers = "for internal use only",
dispatch = "for internal use only",
handleRequest = "for internal use only",
requestFlush = "for internal use only",
saveFileUrl = "for internal use only",
startTiming = "for internal use only",
wsClosed = "for internal use only"
))
@@ -236,9 +233,9 @@ MockShinySession <- R6Class(
progressStack = 'Stack',
#' @field token On a real `ShinySession`, used to identify this instance in URLs.
token = 'character',
#' @field cache The session cache MemoryCache.
#' @field cache The session cache object.
cache = NULL,
#' @field appcache The app cache MemoryCache.
#' @field appcache The app cache object.
appcache = NULL,
#' @field restoreContext Part of bookmarking support in a real
#' `ShinySession` but always `NULL` for a `MockShinySession`.
@@ -249,6 +246,8 @@ MockShinySession <- R6Class(
#' @field user The username of an authenticated user. Always `NULL` for a
#' `MockShinySession`.
user = NULL,
#' @field options A list containing session-level shinyOptions.
options = NULL,
#' @description Create a new MockShinySession.
initialize = function() {
@@ -260,7 +259,7 @@ MockShinySession <- R6Class(
private$file_generators <- fastmap()
private$timer <- MockableTimerCallbacks$new()
self$progressStack <- Stack$new()
self$progressStack <- fastmap::faststack()
self$userData <- new.env(parent=emptyenv())
@@ -273,8 +272,12 @@ MockShinySession <- R6Class(
self$input <- .createReactiveValues(private$.input, readonly = TRUE)
self$token <- createUniqueId(16)
self$cache <- MemoryCache$new()
self$appcache <- MemoryCache$new()
# Copy app-level options
self$options <- getCurrentAppState()$options
self$cache <- cachem::cache_mem()
self$appcache <- cachem::cache_mem()
# Adds various generated noop and error-producing method implementations.
# Note that noop methods can be configured to produce warnings by setting
@@ -433,29 +436,36 @@ MockShinySession <- R6Class(
if (!is.function(func))
stop(paste("Unexpected", class(func), "output for", name))
obs <- observe({
# We could just stash the promise, but we get an "unhandled promise error". This bypasses
prom <- NULL
tryCatch({
v <- private$withCurrentOutput(name, func(self, name))
if (!promises::is.promise(v)){
# Make our sync value into a promise
prom <- promises::promise(function(resolve, reject){ resolve(v) })
} else {
prom <- v
}
}, error=function(e){
# Error running value()
prom <<- promises::promise(function(resolve, reject){ reject(e) })
})
private$outs[[name]]$promise <- hybrid_chain(
prom,
function(v){
list(val = v, err = NULL)
}, catch=function(e){
list(val = NULL, err = e)
with_no_otel_collect({
obs <- observe({
# We could just stash the promise, but we get an "unhandled promise error". This bypasses
prom <- NULL
tryCatch({
v <- private$withCurrentOutput(name, func(self, name))
if (!is.promise(v)){
# Make our sync value into a promise
prom <- promise_resolve(v)
} else {
prom <- v
}
}, error=function(e){
# Error running value()
prom <<- promise_reject(e)
})
private$outs[[name]]$promise <- hybrid_chain(
prom,
function(v){
list(val = v, err = NULL)
}, catch=function(e){
if (
!inherits(e, c("shiny.custom.error", "shiny.output.cancel", "shiny.output.progress", "shiny.silent.error"))
) {
self$unhandledError(e, close = FALSE)
}
list(val = NULL, err = e)
})
})
})
private$outs[[name]] <- list(obs = obs, func = func, promise = NULL)
},
@@ -557,10 +567,26 @@ MockShinySession <- R6Class(
rootScope = function() {
self
},
#' @description Add an unhandled error callback.
#' @param callback The callback to add, which should accept an error object
#' as its first argument.
#' @return A deregistration function.
onUnhandledError = function(callback) {
private$unhandledErrorCallbacks$register(callback)
},
#' @description Called by observers when a reactive expression errors.
#' @param e An error object.
unhandledError = function(e) {
self$close()
#' @param close If `TRUE`, the session will be closed after the error is
#' handled, defaults to `FALSE`.
unhandledError = function(e, close = TRUE) {
if (close) {
class(e) <- c("shiny.error.fatal", class(e))
}
private$unhandledErrorCallbacks$invoke(e, onError = printError)
.globals$onUnhandledErrorCallbacks$invoke(e, onError = printError)
if (close) self$close()
},
#' @description Freeze a value until the flush cycle completes.
#' @param x A `ReactiveValues` object.
@@ -617,6 +643,9 @@ MockShinySession <- R6Class(
flushedCBs = NULL,
# @field endedCBs `Callbacks` called when session ends.
endedCBs = NULL,
# @field unhandledErrorCallbacks `Callbacks` called when an unhandled error
# occurs.
unhandledErrorCallbacks = Callbacks$new(),
# @field timer `MockableTimerCallbacks` called at particular times.
timer = NULL,
# @field was_closed Set to `TRUE` once the session is closed.
@@ -689,7 +718,7 @@ MockShinySession <- R6Class(
stop("Nested calls to withCurrentOutput() are not allowed.")
}
promises::with_promise_domain(
with_promise_domain(
createVarPromiseDomain(private, "currentOutputName", name),
expr
)

View File

@@ -29,19 +29,28 @@ removeModal <- function(session = getDefaultReactiveDomain()) {
#' Create a modal dialog UI
#'
#' This creates the UI for a modal dialog, using Bootstrap's modal class. Modals
#' are typically used for showing important messages, or for presenting UI that
#' requires input from the user, such as a username and password input.
#' @description
#' `modalDialog()` creates the UI for a modal dialog, using Bootstrap's modal
#' class. Modals are typically used for showing important messages, or for
#' presenting UI that requires input from the user, such as a user name and
#' password input.
#'
#' `modalButton()` creates a button that will dismiss the dialog when clicked,
#' typically used when customising the `footer`.
#'
#' @inheritParams actionButton
#' @param ... UI elements for the body of the modal dialog box.
#' @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
#' ways; instead it must be dismissed by clicking on the dismiss button, or
#' ways; instead it must be dismissed by clicking on a `modalButton()`, or
#' from a call to [removeModal()] on the server.
#' @param fade If `FALSE`, the modal dialog will have no fade-in animation
#' (it will simply appear rather than fade in to view).
@@ -145,18 +154,25 @@ removeModal <- function(session = getDefaultReactiveDomain()) {
#' }
#' @export
modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"),
size = c("m", "s", "l"), easyClose = FALSE, fade = TRUE) {
size = c("m", "s", "l", "xl"), easyClose = FALSE, fade = TRUE) {
size <- match.arg(size)
cls <- if (fade) "modal fade" else "modal"
div(id = "shiny-modal", class = cls, tabindex = "-1",
`data-backdrop` = if (!easyClose) "static",
`data-keyboard` = if (!easyClose) "false",
backdrop <- if (!easyClose) "static"
keyboard <- if (!easyClose) "false"
div(
id = "shiny-modal",
class = "modal",
class = if (fade) "fade",
tabindex = "-1",
`data-backdrop` = backdrop,
`data-bs-backdrop` = backdrop,
`data-keyboard` = keyboard,
`data-bs-keyboard` = keyboard,
div(
class = "modal-dialog",
class = switch(size, s = "modal-sm", m = NULL, l = "modal-lg"),
class = switch(size, s = "modal-sm", m = NULL, l = "modal-lg", xl = "modal-xl"),
div(class = "modal-content",
if (!is.null(title)) div(class = "modal-header",
tags$h4(class = "modal-title", title)
@@ -165,19 +181,26 @@ modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"),
if (!is.null(footer)) div(class = "modal-footer", footer)
)
),
tags$script("$('#shiny-modal').modal().focus();")
# jQuery plugin doesn't work in Bootstrap 5, but vanilla JS doesn't work in Bootstrap 4 :sob:
tags$script(HTML(
"if (window.bootstrap && !window.bootstrap.Modal.VERSION.match(/^4\\./)) {
var modal = new bootstrap.Modal(document.getElementById('shiny-modal'));
modal.show();
} else {
$('#shiny-modal').modal().focus();
}"
))
)
}
#' Create a button for a modal dialog
#'
#' When clicked, a `modalButton` will dismiss the modal dialog.
#'
#' @inheritParams actionButton
#' @seealso [modalDialog()] for examples.
#' @export
#' @rdname modalDialog
modalButton <- function(label, icon = NULL) {
tags$button(type = "button", class = "btn btn-default",
`data-dismiss` = "modal", validateIcon(icon), label
tags$button(
type = "button",
class = "btn btn-default",
`data-dismiss` = "modal",
`data-bs-dismiss` = "modal",
validateIcon(icon), label
)
}

View File

@@ -31,17 +31,42 @@ createSessionProxy <- function(parentSession, ...) {
# but not `session$userData <- TRUE`) from within a module
# without any hacks (see PR #1732)
if (identical(x[[name]], value)) return(x)
# Special case for $options (issue #3112)
if (name == "options") {
session <- find_ancestor_session(x)
session[[name]] <- value
return(x)
}
stop("Attempted to assign value on session proxy.")
}
`[[<-.session_proxy` <- `$<-.session_proxy`
# Given a session_proxy, search `parent` recursively to find the real
# ShinySession object. If given a ShinySession, simply return it.
find_ancestor_session <- function(x, depth = 20) {
if (depth < 0) {
stop("ShinySession not found")
}
if (inherits(x, "ShinySession")) {
return(x)
}
if (inherits(x, "session_proxy")) {
return(find_ancestor_session(.subset2(x, "parent"), depth-1))
}
stop("ShinySession not found")
}
#' Shiny modules
#'
#' Shiny's module feature lets you break complicated UI and server logic into
#' smaller, self-contained pieces. Compared to large monolithic Shiny apps,
#' modules are easier to reuse and easier to reason about. See the article at
#' <http://shiny.rstudio.com/articles/modules.html> to learn more.
#' <https://shiny.rstudio.com/articles/modules.html> to learn more.
#'
#' Starting in Shiny 1.5.0, we recommend using `moduleServer` instead of
#' [`callModule()`], because the syntax is a little easier
@@ -55,7 +80,7 @@ createSessionProxy <- function(parentSession, ...) {
#' almost always be used).
#'
#' @return The return value, if any, from executing the module server function
#' @seealso <http://shiny.rstudio.com/articles/modules.html>
#' @seealso <https://shiny.rstudio.com/articles/modules.html>
#'
#' @examples
#' # Define the UI for a module

65
R/otel-attr-srcref.R Normal file
View File

@@ -0,0 +1,65 @@
# Very similar to srcrefFromShinyCall(),
# however, this works when the function does not have a srcref attr set
otel_srcref_attributes <- function(srcref, fn_name = NULL) {
if (is.function(srcref)) {
srcref <- getSrcRefs(srcref)[[1]][[1]]
}
if (is.null(srcref)) {
return(NULL)
}
stopifnot(inherits(srcref, "srcref"))
# Semantic conventions for code: https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/
#
# Inspiration from https://github.com/r-lib/testthat/pull/2087/files#diff-92de3306849d93d6f7e76c5aaa1b0c037e2d716f72848f8a1c70536e0c8a1564R123-R124
filename <- attr(srcref, "srcfile")$filename
dropNulls(list(
"code.function.name" = fn_name,
# Location attrs
"code.file.path" = filename,
"code.line.number" = srcref[1],
"code.column.number" = srcref[2],
# Remove these deprecated location names once Logfire supports the preferred names
# https://github.com/pydantic/logfire/issues/1559
"code.filepath" = filename,
"code.lineno" = srcref[1],
"code.column" = srcref[2]
))
}
#' Get the srcref for the call at the specified stack level
#'
#' If you need to go farther back in the `sys.call()` stack, supply a larger
#' negative number to `which_offset`. The default of 0 gets the immediate
#' caller. `-1` would get the caller's caller, and so on.
#' @param which_offset The stack level to get the call from. Defaults to -1 (the
#' immediate caller).
#' @return An srcref object, or NULL if none is found.
#' @noRd
get_call_srcref <- function(which_offset = 0) {
# Go back one call to account for this function itself
call <- sys.call(which_offset - 1)
srcref <- attr(call, "srcref", exact = TRUE)
srcref
}
append_otel_srcref_attrs <- function(attrs, call_srcref, fn_name) {
if (is.null(call_srcref)) {
return(attrs)
}
srcref_attrs <- otel_srcref_attributes(call_srcref, fn_name)
if (is.null(srcref_attrs)) {
return(attrs)
}
attrs[names(srcref_attrs)] <- srcref_attrs
attrs
}

55
R/otel-collect.R Normal file
View File

@@ -0,0 +1,55 @@
otel_collect_choices <- c(
"none",
"session",
"reactive_update",
"reactivity",
"all"
)
# Check if the collect level is sufficient
otel_collect_is_enabled <- function(
impl_level,
# Listen to option and fall back to the env var
opt_collect_level = getOption("shiny.otel.collect", Sys.getenv("SHINY_OTEL_COLLECT", "all"))
) {
opt_collect_level <- as_otel_collect(opt_collect_level)
which(opt_collect_level == otel_collect_choices) >=
which(impl_level == otel_collect_choices)
}
# Check if tracing is enabled and if the collect level is sufficient
has_otel_collect <- function(collect) {
# Only check pkg author input iff loaded with pkgload
if (IS_SHINY_LOCAL_PKG) {
stopifnot(length(collect) == 1, any(collect == otel_collect_choices))
}
otel_is_tracing_enabled() && otel_collect_is_enabled(collect)
}
# Run expr with otel collection disabled
with_no_otel_collect <- function(expr) {
withOtelCollect("none", expr)
}
## -- Helpers -----------------------------------------------------
# shiny.otel.collect can be:
# "none"; To do nothing / fully opt-out
# "session" for session/start events
# "reactive_update" (includes "session" features) and reactive_update spans
# "reactivity" (includes "reactive_update" features) and spans for all reactive things
# "all" - Anything that Shiny can do. (Currently equivalent to the "reactivity" level)
as_otel_collect <- function(collect = "all") {
if (!is.character(collect)) {
stop("`collect` must be a character vector.")
}
# Match to collect enum
collect <- match.arg(collect, otel_collect_choices, several.ok = FALSE)
return(collect)
}

194
R/otel-enable.R Normal file
View File

@@ -0,0 +1,194 @@
# # Approach
# Use flags on the reactive object to indicate whether to record OpenTelemetry spans.
#
# Cadence:
# * `$.isRecordingOtel` - Whether to record OpenTelemetry spans for this reactive object
# * `$.otelLabel` - The label to use for the OpenTelemetry span
# * `$.otelAttrs` - Additional attributes to add to the OpenTelemetry span
#' Add OpenTelemetry for reactivity to an object
#'
#' @description
#'
#' `enable_otel_*()` methods add OpenTelemetry flags for [reactive()] expressions
#' and `render*` functions (like [renderText()], [renderTable()], ...).
#'
#' Wrapper to creating an active reactive OpenTelemetry span that closes when
#' the reactive expression is done computing. Typically this is when the
#' reactive expression finishes (synchronous) or when the returned promise is
#' done computing (asynchronous).
#' @section Async with OpenTelemetry:
#'
#' With a reactive expression, the key and/or value expression can be
#' _asynchronous_. In other words, they can be promises --- not regular R
#' promises, but rather objects provided by the
#' \href{https://rstudio.github.io/promises/}{\pkg{promises}} package, which
#' are similar to promises in JavaScript. (See [promises::promise()] for more
#' information.) You can also use [mirai::mirai()] or [future::future()]
#' objects to run code in a separate process or even on a remote machine.
#'
#' When reactive expressions are being calculated in parallel (by having
#' another reactive promise compute in the main process), the currently active
#' OpenTelemetry span will be dynamically swapped out according to the
#' currently active reactive expression. This means that as long as a promise
#' was `then()`ed or `catch()`ed with an active OpenTelemetry span, the span
#' will be correctly propagated to the next step (and subsequently other
#' steps) in the promise chain.
#'
#' While the common case is for a reactive expression to be created
#' synchronously, troubles arise when the reactive expression is created
#' asynchronously. The span **must** be created before the reactive expression
#' is executed, it **must** be active for the duration of the expression, and
#' it **must** not be closed until the reactive expression is done executing.
#' This is not easily achieved with a single function call, so we provide a
#' way to create a reactive expression that is bound to an OpenTelemetry
#' span.
#'
#' @section Span management and performance:
#'
#' Dev note - Barret 2025-10:
#' Typically, an OpenTelemetry span (`otel_span`) will inherit from the parent
#' span. This works well and we can think of the hierarchy as a tree. With
#' `options("shiny.otel.collect" = <value>)`, we are able to control with a sliding
#' dial how much of the tree we are interested in: "none", "session",
#' "reactive_update", "reactivity", and finally "all".
#'
#' Leveraging this hierarchy, we can avoid creating spans that are not needed.
#' The act of making a noop span takes on the order of 10microsec. Handling of
#' the opspan is also in the 10s of microsecond range. We should avoid this when
#' we **know** that we're not interested in the span. Therefore, manually
#' handling spans should be considered for Shiny.
#'
#' * Q:
#' * But what about app author who want the current span? Is there any
#' guarantee that the current span is expected `reactive()` span?
#' * A:
#' * No. The current span is whatever the current span is. If the app author
#' wants a specific span, they should create it themselves.
#' * Proof:
#' ```r
#' noop <- otel::get_active_span()
#' noop$get_context()$get_span_id()
#' #> [1] "0000000000000000"
#' ignore <- otelsdk::with_otel_record({
#' a <- otel::start_local_active_span("a")
#' a$get_context()$get_span_id() |> str()
#' otel::with_active_span(noop, {
#' otel::get_active_span()$get_context()$get_span_id() |> str()
#' })
#' })
#' #> chr "2645e95715841e75"
#' #> chr "2645e95715841e75"
#' # ## It is reasonable to expect the second id to be `0000000000000000`, but it's not.
#' ```
#' Therefore, the app author has no guarantee that the current span is the
#' span they're expecting. If the app author wants a specific span, they should
#' create it themselves and let natural inheritance take over.
#'
#' Given this, I will imagine that app authors will set
#' `options("shiny.otel.collect" = "reactive_update")` as their default behavior.
#' Enough to know things are happening, but not overwhelming from **everything**
#' that is reactive.
#'
#' To _light up_ a specific area, users can call `withr::with_options(list("shiny.otel.collect" = "all"), { ... })`.
#'
#' @param x The object to add caching to.
#' @param ... Future parameter expansion.
#' @noRd
NULL
enable_otel_reactive_val <- function(x) {
impl <- attr(x, ".impl", exact = TRUE)
# Set flag for otel logging when setting the value
impl$.isRecordingOtel <- TRUE
class(x) <- c("reactiveVal.otel", class(x))
x
}
enable_otel_reactive_values <- function(x) {
impl <- .subset2(x, "impl")
# Set flag for otel logging when setting values
impl$.isRecordingOtel <- TRUE
class(x) <- c("reactivevalues.otel", class(x))
x
}
enable_otel_reactive_expr <- function(x) {
domain <- reactive_get_domain(x)
impl <- attr(x, "observable", exact = TRUE)
impl$.isRecordingOtel <- TRUE
# Covers both reactive and reactive.event
impl$.otelLabel <- otel_span_label_reactive(x, domain = impl$.domain)
class(x) <- c("reactiveExpr.otel", class(x))
x
}
enable_otel_observe <- function(x) {
x$.isRecordingOtel <- TRUE
x$.otelLabel <- otel_span_label_observer(x, domain = x$.domain)
class(x) <- c("Observer.otel", class(x))
invisible(x)
}
enable_otel_shiny_render_function <- function(x) {
valueFunc <- force(x)
otel_span_label <- NULL
otel_span_attrs <- NULL
renderFunc <- function(...) {
# Dynamically determine the span label given the current reactive domain
if (is.null(otel_span_label)) {
domain <- getDefaultReactiveDomain()
otel_span_label <<-
otel_span_label_render_function(x, domain = domain)
otel_span_attrs <<- c(
attr(x, "otelAttrs"),
otel_session_id_attrs(domain)
)
}
with_otel_span(
otel_span_label,
{
hybrid_then(
valueFunc(...),
on_failure = set_otel_exception_status_and_throw,
# Must save the error object
tee = FALSE
)
},
attributes = otel_span_attrs
)
}
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
class(renderFunc) <- c("shiny.render.function.otel", class(valueFunc))
renderFunc
}
# ## If we ever expose a S3 function, I'd like to add this method.
# bindOtel.function <- function(x, ...) {
# cli::cli_abort(paste0(
# "Don't know how to add OpenTelemetry recording to a plain function. ",
# "If this is a {.code render*()} function for Shiny, it may need to be updated. ",
# "Please see {.help shiny::bindOtel} for more information."
# ))
# }

56
R/otel-error.R Normal file
View File

@@ -0,0 +1,56 @@
has_seen_otel_exception <- function(cnd) {
!is.null(cnd$.shiny_otel_exception)
}
mark_otel_exception_as_seen <- function(cnd) {
cnd$.shiny_otel_exception <- TRUE
cnd
}
set_otel_exception_status_and_throw <- function(cnd) {
cnd <- set_otel_exception_status(cnd)
# Rethrow the (possibly updated) error
signalCondition(cnd)
}
set_otel_exception_status <- function(cnd) {
if (inherits(cnd, "shiny.custom.error")) {
# No-op
} else if (inherits(cnd, "shiny.output.cancel")) {
# No-op
} else if (inherits(cnd, "shiny.output.progress")) {
# No-op
} else if (cnd_inherits(cnd, "shiny.silent.error")) {
# No-op
} else {
# Only when an unknown error occurs do we set the span status to error
span <- otel::get_active_span()
# Only record the exception once at the original point of failure,
# not every reactive expression that it passes through
if (!has_seen_otel_exception(cnd)) {
span$record_exception(
# Record a sanitized error if sanitization is enabled
get_otel_error_obj(cnd)
)
cnd <- mark_otel_exception_as_seen(cnd)
}
# Record the error status on the span for any context touching this error
span$set_status("error")
}
cnd
}
get_otel_error_obj <- function(e) {
# Do not expose errors to otel if sanitization is enabled
if (getOption("shiny.otel.sanitize.errors", TRUE)) {
sanitized_error()
} else {
e
}
}

198
R/otel-label.R Normal file
View File

@@ -0,0 +1,198 @@
# observe mymod:<anonymous>
# observe <anonymous>
# observe mylabel
# -- Reactives --------------------------------------------------------------
#' OpenTelemetry Label Generation Functions
#'
#' Functions for generating formatted labels for OpenTelemetry tracing spans
#' in Shiny applications. These functions handle module namespacing and
#' cache/event modifiers for different Shiny reactive constructs.
#'
#' @param x The object to generate a label for (reactive, observer, etc.)
#' @param label Character string label for reactive values
#' @param key Character string key for reactiveValues operations
#' @param ... Additional arguments (unused)
#' @param domain Shiny domain object containing namespace information
#'
#' @return Character string formatted for OpenTelemetry span labels
#' @name otel_label
#' @noRd
NULL
otel_span_label_reactive <- function(x, ..., domain) {
fn_name <- otel_label_with_modifiers(
x,
"reactive",
cache_class = "reactive.cache",
event_class = "reactive.event"
)
label <- attr(x, "observable", exact = TRUE)[[".label"]]
otel_span_label <- otel_label_upgrade(label, domain = domain)
sprintf("%s %s", fn_name, otel_span_label)
}
otel_span_label_render_function <- function(x, ..., domain) {
fn_name <- otel_label_with_modifiers(
x,
"output",
cache_class = "shiny.render.function.cache",
event_class = "shiny.render.function.event"
)
label <- getCurrentOutputInfo(session = domain)$name %||% "<unknown>"
otel_span_label <- otel_label_upgrade(label, domain = domain)
sprintf("%s %s", fn_name, otel_span_label)
}
otel_span_label_observer <- function(x, ..., domain) {
fn_name <- otel_label_with_modifiers(
x,
"observe",
cache_class = NULL, # Do not match a cache class here
event_class = "Observer.event"
)
otel_span_label <- otel_label_upgrade(x$.label, domain = domain)
sprintf("%s %s", fn_name, otel_span_label)
}
# -- Set reactive value(s) ----------------------------------------------------
otel_log_label_set_reactive_val <- function(label, ..., domain) {
sprintf(
"Set reactiveVal %s",
otel_label_upgrade(label, domain = domain)
)
}
otel_log_label_set_reactive_values <- function(label, key, ..., domain) {
sprintf(
"Set reactiveValues %s$%s",
otel_label_upgrade(label, domain = domain),
key
)
}
# -- ExtendedTask -------------------------------------------------------------
otel_span_label_extended_task <- function(label, suffix = NULL, ..., domain) {
sprintf(
"ExtendedTask %s",
otel_label_upgrade(label, domain = domain)
)
}
otel_log_label_extended_task_add_to_queue <- function(label, ..., domain) {
sprintf(
"ExtendedTask %s add to queue",
otel_label_upgrade(label, domain = domain)
)
}
# -- Debounce / Throttle -------------------------------------------------------
otel_label_debounce <- function(label, ..., domain) {
sprintf(
"debounce %s",
otel_label_upgrade(label, domain = domain)
)
}
otel_label_throttle <- function(label, ..., domain) {
sprintf(
"throttle %s",
otel_label_upgrade(label, domain = domain)
)
}
# ---- Reactive Poll / File Reader -----------------------------------------------
otel_label_reactive_poll <- function(label, ..., domain) {
sprintf(
"reactivePoll %s",
otel_label_upgrade(label, domain = domain)
)
}
otel_label_reactive_file_reader <- function(label, ..., domain) {
sprintf(
"reactiveFileReader %s",
otel_label_upgrade(label, domain = domain)
)
}
# -- Helpers --------------------------------------------------------------
#' Modify function name based on object class modifiers
#'
#' @param x Object to check class of
#' @param fn_name Base function name
#' @param cache_class Optional class name that indicates cache operation
#' @param event_class Optional class name that indicates event operation
#'
#' @return Modified function name with "cache" or "event" suffix if applicable
#' @noRd
otel_label_with_modifiers <- function(
x,
fn_name,
cache_class = NULL,
event_class = NULL
) {
for (x_class in rev(class(x))) {
if (!is.null(cache_class) && x_class == cache_class) {
fn_name <- sprintf("%s cache", fn_name)
} else if (!is.null(event_class) && x_class == event_class) {
fn_name <- sprintf("%s event", fn_name)
}
}
fn_name
}
#' Upgrade and format OpenTelemetry labels with module namespacing
#'
#' Processes labels for OpenTelemetry tracing, replacing default verbose labels
#' with cleaner alternatives and prepending module namespaces when available.
#'
#' @param label Character string label to upgrade
#' @param ... Additional arguments (unused)
#' @param domain Shiny domain object containing namespace information
#'
#' @return Modified label string with module prefix if applicable
#' @noRd
#'
#' @details
#' Module prefix examples:
#' - "" -> ""
#' - "my-nested-mod-" -> "my-nested-mod"
otel_label_upgrade <- function(label, ..., domain) {
# By default, `observe()` sets the label to `observe(CODE)`
# This label is too big and inconsistent.
# Replace it with `<anonymous>`
# (Similar with `eventReactive()` and `observeEvent()`)
if (is_default_label(label) && grepl("(", label, fixed = TRUE)) {
label <- "<anonymous>"
# label <- sprintf("<anonymous> - %s", label)
}
if (is.null(domain)) {
return(label)
}
namespace <- domain$ns("")
if (!nzchar(namespace)) {
return(label)
}
# Remove trailing module separator
mod_ns <- sub(sprintf("%s$", ns.sep), "", namespace)
# Prepend the module name to the label
# Ex: `"mymod:x"`
sprintf("%s:%s", mod_ns, label)
}

114
R/otel-reactive-update.R Normal file
View File

@@ -0,0 +1,114 @@
# * `session$userData[["_otel_span_reactive_update"]]` - The active reactive update span (or `NULL`)
#' Start a `reactive_update` OpenTelemetry span and store it
#'
#' Used when a reactive expression is updated
#' Will only start the span iff the otel tracing is enabled
#' @param ... Ignored
#' @param domain The reactive domain to associate with the span
#' @return Invisibly returns.
#' @seealso `otel_span_reactive_update_teardown()`
#' @noRd
otel_span_reactive_update_init <- function(..., domain) {
if (!has_otel_collect("reactive_update")) return()
# Ensure cleanup is registered only once per session
if (is.null(domain$userData[["_otel_has_reactive_cleanup"]])) {
domain$userData[["_otel_has_reactive_cleanup"]] <- TRUE
# Clean up any dangling reactive spans on an unplanned exit
domain$onSessionEnded(function() {
otel_span_reactive_update_teardown(domain = domain)
})
}
# Safety check
if (is_otel_span(domain$userData[["_otel_span_reactive_update"]])) {
stop("Reactive update span already exists")
}
domain$userData[["_otel_span_reactive_update"]] <-
start_otel_span(
"reactive_update",
...,
attributes = otel_session_id_attrs(domain)
)
invisible()
}
#' End a `reactive_update` OpenTelemetry span and remove it from the session
#' @param ... Ignored
#' @param domain The reactive domain to associate with the span
#' @return Invisibly returns.
#' @seealso `otel_span_reactive_update_init()`
#' @noRd
otel_span_reactive_update_teardown <- function(..., domain) {
ospan <- domain$userData[["_otel_span_reactive_update"]]
if (is_otel_span(ospan)) {
otel::end_span(ospan)
domain$userData[["_otel_span_reactive_update"]] <- NULL
}
invisible()
}
#' Run expr within a `reactive_update` OpenTelemetry span
#'
#' Used to wrap the execution of a reactive expression. Will only
#' require/activate the span iff the otel tracing is enabled
#' @param expr The expression to executed within the span
#' @param ... Ignored
#' @param domain The reactive domain to associate with the span
#' @noRd
with_otel_span_reactive_update <- function(expr, ..., domain) {
ospan <- domain$userData[["_otel_span_reactive_update"]]
if (!is_otel_span(ospan)) {
return(force(expr))
}
# Given the reactive update span is started before and ended when exec count
# is 0, we only need to wrap the expr in the span context
otel::with_active_span(ospan, {force(expr)})
}
#' Run expr within `reactive_update` otel span if not already active
#'
#' If the reactive update otel span is not already active, run the expression
#' within the reactive update otel span context. This ensures that nested calls
#' to reactive expressions do not attempt to re-enter the same span.
#'
#' This method is used within Context `run()` and running an Output's observer
#' implementation
#' @param expr The expression to executed within the span
#' @param ... Ignored
#' @param domain The reactive domain to associate with the span
#' @noRd
maybe_with_otel_span_reactive_update <- function(expr, ..., domain) {
if (is.null(domain$userData[["_otel_reactive_update_is_active"]])) {
domain$userData[["_otel_reactive_update_is_active"]] <- TRUE
# When the expression is done promising, clear the active flag
hybrid_then(
{
with_otel_span_reactive_update(domain = domain, expr)
},
on_success = function(value) {
domain$userData[["_otel_reactive_update_is_active"]] <- NULL
},
on_failure = function(e) {
domain$userData[["_otel_reactive_update_is_active"]] <- NULL
},
# Return the value before the callbacks
tee = TRUE
)
} else {
expr
}
}

96
R/otel-session.R Normal file
View File

@@ -0,0 +1,96 @@
# Semantic conventions for session: https://opentelemetry.io/docs/specs/semconv/general/session/
#' Create and use session span and events
#'
#' If otel is disabled, the session span and events will not be created,
#' however the expression will still be evaluated.
#'
#' Span: `session_start`, `session_end`
#' @param expr Expression to evaluate within the session span
#' @param ... Ignored
#' @param domain The reactive domain
#' @noRd
otel_span_session_start <- function(expr, ..., domain) {
if (!has_otel_collect("session")) {
return(force(expr))
}
# Wrap the server initialization
with_otel_span(
"session_start",
expr,
attributes = otel::as_attributes(c(
otel_session_id_attrs(domain),
otel_session_attrs(domain)
))
)
}
otel_span_session_end <- function(expr, ..., domain) {
if (!has_otel_collect("session")) {
return(force(expr))
}
id_attrs <- otel_session_id_attrs(domain)
with_otel_span(
"session_end",
expr,
attributes = id_attrs
)
}
# -- Helpers -------------------------------
# Occurs when the websocket connection is established
otel_session_attrs <- function(domain) {
# TODO: Future: Posit Connect integration
# > we are still trying to identify all of the information we want to track/expose
#
# * `POSIT_PRODUCT` (Fallback to RSTUDIO_PRODUCT) for host environment
# * `CONNECT_SERVER` envvar to get the `session.address`.
# * `CONNECT_CONTENT_GUID` for the consistent app distinguisher
# * Maybe `CONNECT_CONTENT_JOB_KEY`?
# * Maybe `user.id` to be their user name: https://opentelemetry.io/docs/specs/semconv/registry/attributes/user/
attrs <- list(
server.path =
sub(
"/websocket/$", "/",
domain[["request"]][["PATH_INFO"]] %||% ""
),
server.address = domain[["request"]][["HTTP_HOST"]] %||% "",
server.origin = domain[["request"]][["HTTP_ORIGIN"]] %||% "",
## Currently, Shiny does not expose QUERY_STRING when connecting the websocket
# so we do not provide it here.
# QUERY_STRING = domain[["request"]][["QUERY_STRING"]] %||% "",
server.port = domain[["request"]][["SERVER_PORT"]] %||% NA_integer_
)
# Safely convert SERVER_PORT to integer
# If conversion fails, leave as-is (string or empty)
# This avoids warnings/errors if SERVER_PORT is not a valid integer
server_port <- suppressWarnings(as.integer(attrs$server.port))
if (!is.na(server_port)) {
attrs$server.port <- server_port
}
attrs
}
otel_session_id_attrs <- function(domain) {
token <- domain$token
if (is.null(token)) {
return(list())
}
list(
# Convention for client-side with session.start and session.end events
# https://opentelemetry.io/docs/specs/semconv/general/session/
#
# Since we are the server, we'll add them as an attribute to _every_ span
# within the session as we don't know exactly when they will be called.
# Given it's only a single attribute, the cost should be minimal, but it ties every reactive calculation together.
session.id = token
)
}

127
R/otel-shiny.R Normal file
View File

@@ -0,0 +1,127 @@
# Used by otel to identify the tracer and logger for this package
# https://github.com/r-lib/otel/blob/afc31bc1f4bd177870d44b051ada1d9e4e685346/R/tracer-name.R#L33-L49
# DO NOT CHANGE THIS VALUE without understanding the implications for existing telemetry data!
otel_tracer_name <- "co.posit.r-package.shiny"
init_otel <- function() {
.globals$otel_tracer <- otel::get_tracer()
.globals$otel_is_tracing_enabled <- otel::is_tracing_enabled(.globals$otel_tracer)
.globals$otel_logger <- otel::get_logger()
# .globals$otel_is_logging_enabled <- otel::is_logging_enabled()
}
on_load({init_otel()})
#' Run expr within a Shiny OpenTelemetry recording context
#'
#' Reset the OpenTelemetry tracer and logger for Shiny.
#' Used for testing purposes only.
#' @param expr Expression to evaluate within the recording context
#' @return The result of evaluating `otelsdk::with_otel_record(expr)` with freshly enabled Shiny otel tracer and logger
#' @noRd
with_shiny_otel_record <- function(expr) {
# Only use within internal testthat tests
stopifnot(testthat__is_testing())
withr::defer({ init_otel() })
otelsdk::with_otel_record({
init_otel()
force(expr)
})
}
#' Check if OpenTelemetry tracing is enabled
#'
#' @param tracer The OpenTelemetry tracer to check (default: Shiny otel tracer)
#' @return `TRUE` if tracing is enabled, `FALSE` otherwise
#' @noRd
otel_is_tracing_enabled <- function() {
.globals[["otel_is_tracing_enabled"]]
}
#' Shiny OpenTelemetry logger
#'
#' Used for logging OpenTelemetry events via `otel_log()`
#' @return An OpenTelemetry logger
#' @noRd
shiny_otel_logger <- function() {
.globals[["otel_logger"]]
}
#' Shiny OpenTelemetry tracer
#'
#' Used for creating OpenTelemetry spans via `with_otel_span()` and
#' `start_otel_span()`
#'
#' Inspired by httr2:::get_tracer().
#' @return An OpenTelemetry tracer
#' @noRd
shiny_otel_tracer <- function() {
.globals[["otel_tracer"]]
}
#' Create and use a Shiny OpenTelemetry span
#'
#' If otel is disabled, the span will not be created,
#' however the expression will still be evaluated.
#' @param name Span name
#' @param expr Expression to evaluate within the span
#' @param ... Ignored
#' @param attributes Optional span attributes
#' @return The result of evaluating `expr`
#' @noRd
with_otel_span <- function(name, expr, ..., attributes = NULL) {
promises::with_otel_span(name, expr, ..., attributes = attributes, tracer = shiny_otel_tracer())
}
#' Start a Shiny OpenTelemetry span
#'
#' @param name Span name
#' @param ... Additional arguments passed to `otel::start_span()`
#' @return An OpenTelemetry span
#' @noRd
start_otel_span <- function(name, ...) {
otel::start_span(name, ..., tracer = shiny_otel_tracer())
}
# # TODO: Set attributes on the current active span
# # 5. Set attributes on the current active span
# set_otel_span_attrs(status = 200L)
# -- Helpers --------------------------------------------------------------
is_otel_span <- function(x) {
inherits(x, "otel_span")
}
testthat__is_testing <- function() {
# testthat::is_testing()
identical(Sys.getenv("TESTTHAT"), "true")
}
#' Log a message using the Shiny OpenTelemetry logger
#'
#' @param msg The log message
#' @param ... Additional attributes to add to the log record
#' @param severity The log severity level (default: "info")
#' @param logger The OpenTelemetry logger to use (default: Shiny otel logger)
#' @return Invisibly returns.
#' @noRd
otel_log <- function(
msg,
...,
severity = "info",
logger = shiny_otel_logger()
) {
otel::log(msg, ..., severity = severity, logger = logger)
}

125
R/otel-with.R Normal file
View File

@@ -0,0 +1,125 @@
#' Temporarily set OpenTelemetry (OTel) collection level
#'
#' @description
#' Control Shiny's OTel collection level for particular reactive expression(s).
#'
#' `withOtelCollect()` sets the OpenTelemetry collection level for
#' the duration of evaluating `expr`. `localOtelCollect()` sets the collection
#' level for the remainder of the current function scope.
#'
#' @details
#' Note that `"session"` and `"reactive_update"` levels are not permitted as
#' these are runtime-specific levels that should only be set permanently via
#' `options(shiny.otel.collect = ...)` or the `SHINY_OTEL_COLLECT` environment
#' variable, not temporarily during reactive expression creation.
#'
#' @section Best practice:
#'
#' Best practice is to set the collection level for code that *creates* reactive
#' expressions, not code that *runs* them. For instance:
#'
#' ```r
#' # Disable telemetry for a reactive expression
#' withOtelCollect("none", {
#' my_reactive <- reactive({ ... })
#' })
#'
#' # Disable telemetry for a render function
#' withOtelCollect("none", {
#' output$my_plot <- renderPlot({ ... })
#' })
#'
#' #' # Disable telemetry for an observer
#' withOtelCollect("none", {
#' observe({ ... }))
#' })
#'
#' # Disable telemetry for an entire module
#' withOtelCollect("none", {
#' my_result <- my_module("my_id")
#' })
#' # Use `my_result` as normal here
#' ```
#'
#' NOTE: It's not recommended to pipe existing reactive objects into
#' `withOtelCollect()` since they won't inherit their intended OTel settings,
#' leading to confusion.
#'
#' @param collect Character string specifying the OpenTelemetry collection level.
#' Must be one of the following:
#'
#' * `"none"` - No telemetry data collected
#' * `"reactivity"` - Collect reactive execution spans (includes session and
#' reactive update events)
#' * `"all"` - All available telemetry (currently equivalent to `"reactivity"`)
#' @param expr Expression to evaluate with the specified collection level
#' (for `withOtelCollect()`).
#' @param envir Environment where the collection level should be set
#' (for `localOtelCollect()`). Defaults to the parent frame.
#'
#' @return
#' * `withOtelCollect()` returns the value of `expr`.
#' * `localOtelCollect()` is called for its side effect and returns the previous
#' `collect` value invisibly.
#'
#' @seealso See the `shiny.otel.collect` option within [`shinyOptions`]. Setting
#' this value will globally control OpenTelemetry collection levels.
#'
#' @examples
#' \dontrun{
#' # Temporarily disable telemetry collection
#' withOtelCollect("none", {
#' # Code here won't generate telemetry
#' reactive({ input$x + 1 })
#' })
#'
#' # Collect reactivity telemetry but not other events
#' withOtelCollect("reactivity", {
#' # Reactive execution will be traced
#' observe({ print(input$x) })
#' })
#'
#' # Use local variant in a function
#' my_function <- function() {
#' localOtelCollect("none")
#' # Rest of function executes without telemetry
#' reactive({ input$y * 2 })
#' }
#' }
#'
#' @rdname withOtelCollect
#' @export
withOtelCollect <- function(collect, expr) {
collect <- as_otel_collect_with(collect)
withr::with_options(
list(shiny.otel.collect = collect),
expr
)
}
#' @rdname withOtelCollect
#' @export
localOtelCollect <- function(collect, envir = parent.frame()) {
collect <- as_otel_collect_with(collect)
old <- withr::local_options(
list(shiny.otel.collect = collect),
.local_envir = envir
)
invisible(old)
}
# Helper function to validate collect levels for with/local functions
# Only allows "none", "reactivity", and "all" - not "session" or "reactive_update"
as_otel_collect_with <- function(collect) {
if (!is.character(collect)) {
stop("`collect` must be a character vector.")
}
allowed_levels <- c("none", "reactivity", "all")
collect <- match.arg(collect, allowed_levels, several.ok = FALSE)
return(collect)
}

View File

@@ -76,8 +76,10 @@ Progress <- R6Class(
min = 0, max = 1,
style = getShinyOption("progress.style", default = "notification"))
{
if (is.null(session))
rlang::abort("Can only use Progress$new() inside a Shiny app")
if (is.null(session$progressStack))
stop("'session' is not a ShinySession object.")
rlang::abort("`session` is not a ShinySession object.")
private$session <- session
private$id <- createUniqueId(8)
@@ -204,7 +206,7 @@ Progress <- R6Class(
#' the server function. The default is to automatically find the session by
#' using the current reactive domain.
#' @param expr The work to be done. This expression should contain calls to
#' `setProgress`.
#' [setProgress()] or [incProgress()].
#' @param min The value that represents the starting point of the progress bar.
#' Must be less tham `max`. Default is 0.
#' @param max The value that represents the end of the progress bar. Must be
@@ -227,6 +229,7 @@ Progress <- R6Class(
#' @param value Single-element numeric vector; the value at which to set the
#' progress bar, relative to `min` and `max`.
#'
#' @return The result of `expr`.
#' @examples
#' ## Only run examples in interactive R sessions
#' if (interactive()) {

117
R/react.R
View File

@@ -5,7 +5,7 @@ processId <- local({
cached <- NULL
function() {
if (is.null(cached)) {
cached <<- digest::digest(list(
cached <<- rlang::hash(list(
Sys.info(),
Sys.time()
))
@@ -16,6 +16,60 @@ processId <- local({
}
})
ctx_otel_info_obj <- function(
isRecordingOtel = FALSE,
otelLabel = "<unknown>",
otelAttrs = list()
) {
structure(
list(
isRecordingOtel = isRecordingOtel,
otelLabel = otelLabel,
otelAttrs = otelAttrs
),
class = "ctx_otel_info"
)
}
with_otel_span_context <- function(otel_info, expr, domain) {
if (!otel_is_tracing_enabled()) {
return(force(expr))
}
isRecordingOtel <- .subset2(otel_info, "isRecordingOtel")
otelLabel <- .subset2(otel_info, "otelLabel")
otelAttrs <- .subset2(otel_info, "otelAttrs")
# Always set the reactive update span as active
# This ensures that any spans created within the reactive context
# are at least children of the reactive update span
maybe_with_otel_span_reactive_update(domain = domain, {
if (isRecordingOtel) {
with_otel_span(
otelLabel,
{
# Works with both sync and async expressions
# Needed for both observer and reactive contexts
hybrid_then(
expr,
on_failure = set_otel_exception_status_and_throw,
# Must upgrade the error object
tee = FALSE
)
},
# expr,
attributes = otelAttrs
)
} else {
force(expr)
}
})
}
#' @include graph.R
Context <- R6Class(
'Context',
@@ -33,11 +87,14 @@ Context <- R6Class(
.pid = NULL,
.weak = NULL,
.otel_info = NULL,
initialize = function(
domain, label='', type='other', prevId='',
reactId = rLog$noReactId,
id = .getReactiveEnvironment()$nextId(), # For dummy context
weak = FALSE
weak = FALSE,
otel_info = ctx_otel_info_obj()
) {
id <<- id
.label <<- label
@@ -47,16 +104,27 @@ Context <- R6Class(
.reactType <<- type
.weak <<- weak
rLog$createContext(id, label, type, prevId, domain)
if (!is.null(otel_info)) {
if (IS_SHINY_LOCAL_PKG) {
stopifnot(inherits(otel_info, "ctx_otel_info"))
}
.otel_info <<- otel_info
}
},
run = function(func) {
"Run the provided function under this context."
# Use `promises::` as it shows up in the stack trace
promises::with_promise_domain(reactivePromiseDomain(), {
withReactiveDomain(.domain, {
env <- .getReactiveEnvironment()
rLog$enter(.reactId, id, .reactType, .domain)
on.exit(rLog$exit(.reactId, id, .reactType, .domain), add = TRUE)
env$runWith(self, func)
with_otel_span_context(.otel_info, domain = .domain, {
captureStackTraces({
env <- .getReactiveEnvironment()
rLog$enter(.reactId, id, .reactType, .domain)
on.exit(rLog$exit(.reactId, id, .reactType, .domain), add = TRUE)
env$runWith(self, func)
})
})
})
})
},
@@ -65,7 +133,7 @@ Context <- R6Class(
that have been registered with onInvalidate()."
if (!identical(.pid, processId())) {
stop("Reactive context was created in one process and invalidated from another")
rlang::abort("Reactive context was created in one process and invalidated from another.")
}
if (.invalidated)
@@ -87,7 +155,7 @@ Context <- R6Class(
immediately."
if (!identical(.pid, processId())) {
stop("Reactive context was created in one process and accessed from another")
rlang::abort("Reactive context was created in one process and accessed from another.")
}
if (.invalidated)
@@ -140,9 +208,13 @@ ReactiveEnvironment <- R6Class(
if (isTRUE(getOption('shiny.suppressMissingContextError'))) {
return(getDummyContext())
} else {
stop('Operation not allowed without an active reactive context. ',
'(You tried to do something that can only be done from inside a ',
'reactive expression or observer.)')
rlang::abort(c(
'Operation not allowed without an active reactive context.',
paste0(
'You tried to do something that can only be done from inside a ',
'reactive consumer.'
)
))
}
}
return(.currentContext)
@@ -202,7 +274,8 @@ getCurrentContext <- function() {
.getReactiveEnvironment()$currentContext()
}
hasCurrentContext <- function() {
!is.null(.getReactiveEnvironment()$.currentContext)
!is.null(.getReactiveEnvironment()$.currentContext) ||
isTRUE(getOption("shiny.suppressMissingContextError"))
}
getDummyContext <- function() {
@@ -214,27 +287,31 @@ getDummyContext <- function() {
wrapForContext <- function(func, ctx) {
force(func)
force(ctx)
force(ctx) # may be NULL (in the case of maskReactiveContext())
function(...) {
ctx$run(function() {
captureStackTraces(
func(...)
)
.getReactiveEnvironment()$runWith(ctx, function() {
func(...)
})
}
}
reactivePromiseDomain <- function() {
promises::new_promise_domain(
new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
ctx <- getCurrentContext()
# ctx will be NULL if we're in a maskReactiveContext()
ctx <- if (hasCurrentContext()) getCurrentContext() else NULL
wrapForContext(onFulfilled, ctx)
},
wrapOnRejected = function(onRejected) {
force(onRejected)
ctx <- getCurrentContext()
# ctx will be NULL if we're in a maskReactiveContext()
ctx <- if (hasCurrentContext()) getCurrentContext() else NULL
wrapForContext(onRejected, ctx)
}
)

View File

@@ -45,6 +45,8 @@ createMockDomain <- function() {
callbacks <- Callbacks$new()
ended <- FALSE
domain <- new.env(parent = emptyenv())
domain$ns <- function(id) id
domain$token <- "mock-domain"
domain$onEnded <- function(callback) {
return(callbacks$register(callback))
}
@@ -95,7 +97,11 @@ getDefaultReactiveDomain <- function() {
#' @rdname domains
#' @export
withReactiveDomain <- function(domain, expr) {
promises::with_promise_domain(createVarPromiseDomain(.globals, "domain", domain), expr)
# Use `promises::` as it shows up in the stack trace
promises::with_promise_domain(
createVarPromiseDomain(.globals, "domain", domain),
expr
)
}
#

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
####
# Generated by `./tools/updateReexports.R`: do not edit by hand
# Please call `source('tools/updateReexports.R') from the root folder to update`
# Generated by `./tools/documentation/updateReexports.R`: do not edit by hand
# Please call `source('tools/documentation/updateReexports.R')` from the root folder to update`
####
@@ -90,17 +90,20 @@ htmltools::em
#' @export
htmltools::hr
# htmltools tag.Rd -------------------------------------------------------------
#' @importFrom htmltools tag
#' @export
htmltools::tag
# htmltools tagList.Rd ---------------------------------------------------------
#' @importFrom htmltools tagList
#' @export
htmltools::tagList
# htmltools tagAppendAttributes.Rd ---------------------------------------------
#' @importFrom htmltools tagAppendAttributes
#' @export
htmltools::tagAppendAttributes
@@ -113,6 +116,9 @@ htmltools::tagHasAttribute
#' @export
htmltools::tagGetAttribute
# htmltools tagAppendChild.Rd --------------------------------------------------
#' @importFrom htmltools tagAppendChild
#' @export
htmltools::tagAppendChild

View File

@@ -1,6 +1,7 @@
#' Plot output with cached images
#'
#' Renders a reactive plot, with plot images cached to disk.
#' Renders a reactive plot, with plot images cached to disk. As of Shiny 1.6.0,
#' this is a shortcut for using [bindCache()] with [renderPlot()].
#'
#' `expr` is an expression that generates a plot, similar to that in
#' `renderPlot`. Unlike with `renderPlot`, this expression does not
@@ -8,7 +9,7 @@
#' changes.
#'
#' `cacheKeyExpr` is an expression which, when evaluated, returns an object
#' which will be serialized and hashed using the [digest::digest()]
#' which will be serialized and hashed using the [rlang::hash()]
#' function to generate a string that will be used as a cache key. This key is
#' used to identify the contents of the plot: if the cache key is the same as a
#' previous time, it assumes that the plot is the same and can be retrieved from
@@ -32,7 +33,7 @@
#' to normal R objects before returning them. Your expression could even
#' serialize and hash that information in an efficient way and return a string,
#' which will in turn be hashed (very quickly) by the
#' [digest::digest()] function.
#' [rlang::hash()] function.
#'
#' Internally, the result from `cacheKeyExpr` is combined with the name of
#' the output (if you assign it to `output$plot1`, it will be combined
@@ -40,95 +41,6 @@
#' if there are multiple plots that have the same `cacheKeyExpr`, they
#' will not have cache key collisions.
#'
#' @section Cache scoping:
#'
#' There are a number of different ways you may want to scope the cache. For
#' example, you may want each user session to have their own plot cache, or
#' you may want each run of the application to have a cache (shared among
#' possibly multiple simultaneous user sessions), or you may want to have a
#' cache that persists even after the application is shut down and started
#' again.
#'
#' To control the scope of the cache, use the `cache` parameter. There
#' are two ways of having Shiny automatically create and clean up the disk
#' cache.
#'
#' \describe{
#' \item{1}{To scope the cache to one run of a Shiny application (shared
#' among possibly multiple user sessions), use `cache="app"`. This
#' is the default. The cache will be shared across multiple sessions, so
#' there is potentially a large performance benefit if there are many users
#' of the application. When the application stops running, the cache will
#' be deleted. If plots cannot be safely shared across users, this should
#' not be used.}
#' \item{2}{To scope the cache to one session, use `cache="session"`.
#' When a new user session starts --- in other words, when a web browser
#' visits the Shiny application --- a new cache will be created on disk
#' for that session. When the session ends, the cache will be deleted.
#' The cache will not be shared across multiple sessions.}
#' }
#'
#' If either `"app"` or `"session"` is used, the cache will be 10 MB
#' in size, and will be stored stored in memory, using a
#' [memoryCache()] object. Note that the cache space will be shared
#' among all cached plots within a single application or session.
#'
#' In some cases, you may want more control over the caching behavior. For
#' example, you may want to use a larger or smaller cache, share a cache
#' among multiple R processes, or you may want the cache to persist across
#' multiple runs of an application, or even across multiple R processes.
#'
#' To use different settings for an application-scoped cache, you can call
#' [shinyOptions()] at the top of your app.R, server.R, or
#' global.R. For example, this will create a cache with 20 MB of space
#' instead of the default 10 MB:
#' \preformatted{
#' shinyOptions(cache = memoryCache(size = 20e6))
#' }
#'
#' To use different settings for a session-scoped cache, you can call
#' [shinyOptions()] at the top of your server function. To use
#' the session-scoped cache, you must also call `renderCachedPlot` with
#' `cache="session"`. This will create a 20 MB cache for the session:
#' \preformatted{
#' function(input, output, session) {
#' shinyOptions(cache = memoryCache(size = 20e6))
#'
#' output$plot <- renderCachedPlot(
#' ...,
#' cache = "session"
#' )
#' }
#' }
#'
#' If you want to create a cache that is shared across multiple concurrent
#' R processes, you can use a [diskCache()]. You can create an
#' application-level shared cache by putting this at the top of your app.R,
#' server.R, or global.R:
#' \preformatted{
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
#' }
#'
#' This will create a subdirectory in your system temp directory named
#' `myapp-cache` (replace `myapp-cache` with a unique name of
#' your choosing). On most platforms, this directory will be removed when
#' your system reboots. This cache will persist across multiple starts and
#' stops of the R process, as long as you do not reboot.
#'
#' To have the cache persist even across multiple reboots, you can create the
#' cache in a location outside of the temp directory. For example, it could
#' be a subdirectory of the application:
#' \preformatted{
#' shinyOptions(cache = diskCache("./myapp-cache"))
#' }
#'
#' In this case, resetting the cache will have to be done manually, by deleting
#' the directory.
#'
#' You can also scope a cache to just one plot, or selected plots. To do that,
#' create a [memoryCache()] or [diskCache()], and pass it
#' as the `cache` argument of `renderCachedPlot`.
#'
#' @section Interactive plots:
#'
#' `renderCachedPlot` can be used to create interactive plots. See
@@ -136,6 +48,7 @@
#'
#'
#' @inheritParams renderPlot
#' @inheritParams bindCache
#' @param cacheKeyExpr An expression that returns a cache key. This key should
#' be a unique identifier for a plot: the assumption is that if the cache key
#' is the same, then the plot will be the same.
@@ -146,14 +59,13 @@
#' possible pixel dimension. See [sizeGrowthRatio()] for more
#' information on the default sizing policy.
#' @param res The resolution of the PNG, in pixels per inch.
#' @param cache The scope of the cache, or a cache object. This can be
#' `"app"` (the default), `"session"`, or a cache object like
#' a [diskCache()]. See the Cache Scoping section for more
#' information.
#' @param width,height not used. They are specified via the argument
#' `sizePolicy`.
#'
#' @seealso See [renderPlot()] for the regular, non-cached version of
#' this function. For more about configuring caches, see
#' [memoryCache()] and [diskCache()].
#' @seealso See [renderPlot()] for the regular, non-cached version of this
#' function. It can be used with [bindCache()] to get the same effect as
#' `renderCachedPlot()`. For more about configuring caches, see
#' [cachem::cache_mem()] and [cachem::cache_disk()].
#'
#'
#' @examples
@@ -244,7 +156,7 @@
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
#' },
#' cacheKeyExpr = { list(input$n) },
#' cache = memoryCache()
#' cache = cachem::cache_mem()
#' )
#' output$plot2 <- renderCachedPlot({
#' Sys.sleep(2) # Add an artificial delay
@@ -253,7 +165,7 @@
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
#' },
#' cacheKeyExpr = { list(input$n) },
#' cache = memoryCache()
#' cache = cachem::cache_mem()
#' )
#' }
#' )
@@ -264,22 +176,22 @@
#' # At the top of app.R, this set the application-scoped cache to be a memory
#' # cache that is 20 MB in size, and where cached objects expire after one
#' # hour.
#' shinyOptions(cache = memoryCache(max_size = 20e6, max_age = 3600))
#' shinyOptions(cache = cachem::cache_mem(max_size = 20e6, max_age = 3600))
#'
#' # At the top of app.R, this set the application-scoped cache to be a disk
#' # cache that can be shared among multiple concurrent R processes, and is
#' # deleted when the system reboots.
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
#' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache")))
#'
#' # At the top of app.R, this set the application-scoped cache to be a disk
#' # cache that can be shared among multiple concurrent R processes, and
#' # persists on disk across reboots.
#' shinyOptions(cache = diskCache("./myapp-cache"))
#' shinyOptions(cache = cachem::cache_disk("./myapp-cache"))
#'
#' # At the top of the server function, this set the session-scoped cache to be
#' # a memory cache that is 5 MB in size.
#' server <- function(input, output, session) {
#' shinyOptions(cache = memoryCache(max_size = 5e6))
#' shinyOptions(cache = cachem::cache_mem(max_size = 5e6))
#'
#' output$plot <- renderCachedPlot(
#' ...,
@@ -295,257 +207,35 @@ renderCachedPlot <- function(expr,
res = 72,
cache = "app",
...,
outputArgs = list()
alt = "Plot object",
outputArgs = list(),
width = NULL,
height = NULL
) {
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
# is called
installExprFunction(expr, "func", parent.frame(), quoted = FALSE, ..stacktraceon = TRUE)
# This is so that the expr doesn't re-execute by itself; it needs to be
# triggered by the cache key (or width/height) changing.
isolatedFunc <- function() isolate(func())
args <- list(...)
expr <- substitute(expr)
if (!is_quosure(expr)) {
expr <- new_quosure(expr, env = parent.frame())
}
cacheKeyExpr <- substitute(cacheKeyExpr)
# The real cache key we'll use also includes width, height, res, pixelratio.
# This is just the part supplied by the user.
userCacheKey <- reactive(cacheKeyExpr, env = parent.frame(), quoted = TRUE, label = "userCacheKey")
ensureCacheSetup <- function() {
# For our purposes, cache objects must support these methods.
isCacheObject <- function(x) {
# Use tryCatch in case the object does not support `$`.
tryCatch(
is.function(x$get) && is.function(x$set),
error = function(e) FALSE
)
}
if (isCacheObject(cache)) {
# If `cache` is already a cache object, do nothing
return()
} else if (identical(cache, "app")) {
cache <<- getShinyOption("cache")
} else if (identical(cache, "session")) {
cache <<- session$cache
} else {
stop('`cache` must either be "app", "session", or a cache object with methods, `$get`, and `$set`.')
}
if (!is_quosure(cacheKeyExpr)) {
cacheKeyExpr <- new_quosure(cacheKeyExpr, env = parent.frame())
}
# The width and height of the plot to draw, given from sizePolicy. These
# values get filled by an observer below.
fitDims <- reactiveValues(width = NULL, height = NULL)
resizeObserver <- NULL
ensureResizeObserver <- function() {
if (!is.null(resizeObserver))
return()
# Given the actual width/height of the image in the browser, this gets the
# width/height from sizePolicy() and pushes those values into `fitDims`.
# It's done this way so that the `fitDims` only change (and cause
# invalidations) when the rendered image size changes, and not every time
# the browser's <img> tag changes size.
doResizeCheck <- function() {
width <- session$clientData[[paste0('output_', outputName, '_width')]]
height <- session$clientData[[paste0('output_', outputName, '_height')]]
if (is.null(width)) width <- 0
if (is.null(height)) height <- 0
rect <- sizePolicy(c(width, height))
fitDims$width <- rect[1]
fitDims$height <- rect[2]
}
# Run it once immediately, then set up the observer
isolate(doResizeCheck())
resizeObserver <<- observe(doResizeCheck())
if (!is.null(width) || !is.null(height)) {
warning("Unused argument(s) 'width' and/or 'height'. ",
"'sizePolicy' is used instead.")
}
# Vars to store session and output, so that they can be accessed from
# the plotObj() reactive.
session <- NULL
outputName <- NULL
drawReactive <- reactive(label = "plotObj", {
hybrid_chain(
# Depend on the user cache key, even though we don't use the value. When
# it changes, it can cause the drawReactive to re-execute. (Though
# drawReactive will not necessarily re-execute --- it must be called from
# renderFunc, which happens only if there's a cache miss.)
userCacheKey(),
function(userCacheKeyValue) {
# Get width/height, but don't depend on them.
isolate({
width <- fitDims$width
height <- fitDims$height
})
pixelratio <- session$clientData$pixelratio %OR% 1
do.call("drawPlot", c(
list(
name = outputName,
session = session,
func = isolatedFunc,
width = width,
height = height,
pixelratio = pixelratio,
res = res
),
args
))
},
catch = function(reason) {
# Non-isolating read. A common reason for errors in plotting is because
# the dimensions are too small. By taking a dependency on width/height,
# we can try again if the plot output element changes size.
fitDims$width
fitDims$height
# Propagate the error
stop(reason)
}
inject(
bindCache(
renderPlot(!!expr, res = res, alt = alt, outputArgs = outputArgs, ...),
!!cacheKeyExpr,
sizePolicy = sizePolicy,
cache = cache
)
})
# This function is the one that's returned from renderPlot(), and gets
# wrapped in an observer when the output value is assigned.
renderFunc <- function(shinysession, name, ...) {
outputName <<- name
session <<- shinysession
ensureCacheSetup()
ensureResizeObserver()
hybrid_chain(
# This use of the userCacheKey() sets up the reactive dependency that
# causes plot re-draw events. These may involve pulling from the cache,
# replaying a display list, or re-executing user code.
userCacheKey(),
function(userCacheKeyResult) {
width <- fitDims$width
height <- fitDims$height
pixelratio <- session$clientData$pixelratio %OR% 1
key <- digest::digest(list(outputName, userCacheKeyResult, width, height, res, pixelratio), "xxhash64")
plotObj <- cache$get(key)
# First look in cache.
# Case 1. cache hit.
if (!is.key_missing(plotObj)) {
return(list(
cacheHit = TRUE,
key = key,
plotObj = plotObj,
width = width,
height = height,
pixelratio = pixelratio
))
}
# If not in cache, hybrid_chain call to drawReactive
#
# Two more possible cases:
# 2. drawReactive will re-execute and return a plot that's the
# correct size.
# 3. It will not re-execute, but it will return the previous value,
# which is the wrong size. It will include a valid display list
# which can be used by resizeSavedPlot.
hybrid_chain(
drawReactive(),
function(drawReactiveResult) {
# Pass along the key for caching in the next stage
list(
cacheHit = FALSE,
key = key,
plotObj = drawReactiveResult,
width = width,
height = height,
pixelratio = pixelratio
)
}
)
},
function(possiblyAsyncResult) {
hybrid_chain(possiblyAsyncResult, function(result) {
width <- result$width
height <- result$height
pixelratio <- result$pixelratio
# Three possibilities when we get here:
# 1. There was a cache hit. No need to set a value in the cache.
# 2. There was a cache miss, and the plotObj is already the correct
# size (because drawReactive re-executed). In this case, we need
# to cache it.
# 3. There was a cache miss, and the plotObj was not the corect size.
# In this case, we need to replay the display list, and then cache
# the result.
if (!result$cacheHit) {
# If the image is already the correct size, this just returns the
# object unchanged.
result$plotObj <- do.call("resizeSavedPlot", c(
list(
name,
shinysession,
result$plotObj,
width,
height,
pixelratio,
res
),
args
))
# Save a cached copy of the plotObj. The recorded displaylist for
# the plot can't be serialized and restored properly within the same
# R session, so we NULL it out before saving. (The image data and
# other metadata be saved and restored just fine.) Displaylists can
# also be very large (~1.5MB for a basic ggplot), and they would not
# be commonly used. Note that displaylist serialization was fixed in
# revision 74506 (2e6c669), and should be in R 3.6. A MemoryCache
# doesn't need to serialize objects, so it could actually save a
# display list, but for the reasons listed previously, it's
# generally not worth it.
# The plotResult is not the same as the recordedPlot (it is used to
# retrieve coordmap information for ggplot2 objects) but it is only
# used in conjunction with the recordedPlot, and we'll remove it
# because it can be quite large.
result$plotObj$plotResult <- NULL
result$plotObj$recordedPlot <- NULL
cache$set(result$key, result$plotObj)
}
img <- result$plotObj$img
# Replace exact pixel dimensions; instead, the max-height and
# max-width will be set to 100% from CSS.
img$class <- "shiny-scalable"
img$width <- NULL
img$height <- NULL
img
})
}
)
}
# If renderPlot isn't going to adapt to the height of the div, then the
# div needs to adapt to the height of renderPlot. By default, plotOutput
# sets the height to 400px, so to make it adapt we need to override it
# with NULL.
outputFunc <- plotOutput
formals(outputFunc)['height'] <- list(NULL)
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
)
}

View File

@@ -34,13 +34,19 @@
#' When rendering an inline plot, you must provide numeric values (in pixels)
#' to both \code{width} and \code{height}.
#' @param res Resolution of resulting plot, in pixels per inch. This value is
#' passed to [grDevices::png()]. Note that this affects the resolution of PNG
#' passed to [plotPNG()]. Note that this affects the resolution of PNG
#' rendering in R; it won't change the actual ppi of the browser.
#' @param ... Arguments to be passed through to [grDevices::png()].
#' @param alt Alternate text for the HTML `<img>` tag if it cannot be displayed
#' or viewed (i.e., the user uses a screen reader). In addition to a character
#' string, the value may be a reactive expression (or a function referencing
#' reactive values) that returns a character string. If the value is `NA` (the
#' default), then `ggplot2::get_alt_text()` is used to extract alt text from
#' ggplot objects; for other plots, `NA` results in alt text of "Plot object".
#' `NULL` or `""` is not recommended because those should be limited to
#' decorative images.
#' @param ... Arguments to be passed through to [plotPNG()].
#' These can be used to set the width, height, background color, etc.
#' @param env The environment in which to evaluate `expr`.
#' @param quoted Is `expr` a quoted expression (with `quote()`)? This
#' is useful if you want to save an expression in a variable.
#' @inheritParams renderUI
#' @param execOnResize If `FALSE` (the default), then when a plot is
#' resized, Shiny will *replay* the plot drawing commands with
#' [grDevices::replayPlot()] instead of re-executing `expr`.
@@ -51,13 +57,19 @@
#' call to [plotOutput()] when `renderPlot` is used in an
#' interactive R Markdown document.
#' @export
renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
env=parent.frame(), quoted=FALSE,
execOnResize=FALSE, outputArgs=list()
renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
alt = NA,
env = parent.frame(), quoted = FALSE,
execOnResize = FALSE, outputArgs = list()
) {
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
# is called
installExprFunction(expr, "func", env, quoted, ..stacktraceon = TRUE)
func <- installExprFunction(
expr, "func", env, quoted,
label = "renderPlot",
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
# is called
..stacktraceon = TRUE
)
args <- list(...)
@@ -75,7 +87,16 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
else
heightWrapper <- function() { height }
getDims <- function() {
if (is.reactive(alt))
altWrapper <- alt
else if (is.function(alt))
altWrapper <- reactive({ alt() })
else
altWrapper <- function() { alt }
# This is the function that will be used as getDims by default, but it can be
# overridden (which happens when bindCache() is used).
getDimsDefault <- function() {
width <- widthWrapper()
height <- heightWrapper()
@@ -94,6 +115,7 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
# the plotObj() reactive.
session <- NULL
outputName <- NULL
getDims <- NULL
# Calls drawPlot, invoking the user-provided `func` (which may or may not
# return a promise). The idea is that the (cached) return value from this
@@ -104,7 +126,7 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
{
# If !execOnResize, don't invalidate when width/height changes.
dims <- if (execOnResize) getDims() else isolate(getDims())
pixelratio <- session$clientData$pixelratio %OR% 1
pixelratio <- session$clientData$pixelratio %||% 1
do.call("drawPlot", c(
list(
name = outputName,
@@ -112,6 +134,7 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
func = func,
width = dims$width,
height = dims$height,
alt = altWrapper(),
pixelratio = pixelratio,
res = res
), args))
@@ -130,17 +153,21 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
# This function is the one that's returned from renderPlot(), and gets
# wrapped in an observer when the output value is assigned.
renderFunc <- function(shinysession, name, ...) {
# The `get_dims` parameter defaults to `getDimsDefault`. However, it can be
# overridden, so that `bindCache` can use a different version.
renderFunc <- function(shinysession, name, ..., get_dims = getDimsDefault) {
outputName <<- name
session <<- shinysession
if (is.null(getDims)) getDims <<- get_dims
hybrid_chain(
drawReactive(),
function(result) {
dims <- getDims()
pixelratio <- session$clientData$pixelratio %OR% 1
pixelratio <- session$clientData$pixelratio %||% 1
result <- do.call("resizeSavedPlot", c(
list(name, shinysession, result, dims$width, dims$height, pixelratio, res),
list(name, shinysession, result, dims$width, dims$height, altWrapper(), pixelratio, res),
args
))
@@ -156,12 +183,19 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
outputFunc <- plotOutput
if (!identical(height, 'auto')) formals(outputFunc)['height'] <- list(NULL)
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
markedFunc <- markRenderFunction(
outputFunc,
renderFunc,
outputArgs,
cacheHint = list(userExpr = installedFuncExpr(func), res = res)
)
class(markedFunc) <- c("shiny.renderPlot", class(markedFunc))
markedFunc
}
resizeSavedPlot <- function(name, session, result, width, height, pixelratio, res, ...) {
if (result$img$width == width && result$img$height == height &&
result$pixelratio == pixelratio && result$res == res) {
resizeSavedPlot <- function(name, session, result, width, height, alt, pixelratio, res, ...) {
if (isTRUE(result$img$width == width && result$img$height == height &&
result$pixelratio == pixelratio && result$res == res)) {
return(result)
}
@@ -181,6 +215,7 @@ resizeSavedPlot <- function(name, session, result, width, height, pixelratio, re
src = session$fileUrl(name, outfile, contentType = "image/png"),
width = width,
height = height,
alt = result$alt,
coordmap = coordmap,
error = attr(coordmap, "error", exact = TRUE)
)
@@ -188,7 +223,7 @@ resizeSavedPlot <- function(name, session, result, width, height, pixelratio, re
result
}
drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
drawPlot <- function(name, session, func, width, height, alt, pixelratio, res, ...) {
# 1. Start PNG
# 2. Enable displaylist recording
# 3. Call user-defined func
@@ -218,11 +253,12 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
hybrid_chain(
hybrid_chain(
promises::with_promise_domain(domain, {
with_promise_domain(domain, {
hybrid_chain(
func(),
function(value, .visible) {
if (.visible) {
function(value) {
res <- withVisible(value)
if (res$visible) {
# A modified version of print.ggplot which returns the built ggplot object
# as well as the gtable grob. This overrides the ggplot::print.ggplot
# method, but only within the context of renderPlot. The reason this needs
@@ -230,6 +266,8 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
# addition to ggplot, and there's a print method for that class, that we
# won't override that method. https://github.com/rstudio/shiny/issues/841
print.ggplot <- custom_print.ggplot
# For compatibility with ggplot2 >v4.0.0
`print.ggplot2::ggplot` <- custom_print.ggplot
# Use capture.output to squelch printing to the actual console; we
# are only interested in plot output
@@ -240,7 +278,7 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
# similar to ggplot2. But for base graphics, it would already have
# been rendered when func was called above, and the print should
# have no effect.
result <- ..stacktraceon..(print(value))
result <- ..stacktraceon..(print(res$value))
# TODO jcheng 2017-04-11: Verify above ..stacktraceon..
})
result
@@ -255,6 +293,7 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
recordedPlot = grDevices::recordPlot(),
coordmap = getCoordmap(value, width*pixelratio, height*pixelratio, res*pixelratio),
pixelratio = pixelratio,
alt = if (anyNA(alt)) getAltText(value) else alt,
res = res
)
}
@@ -269,9 +308,10 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
),
function(result) {
result$img <- dropNulls(list(
src = session$fileUrl(name, outfile, contentType='image/png'),
src = session$fileUrl(name, outfile, contentType = 'image/png'),
width = width,
height = height,
alt = result$alt,
coordmap = result$coordmap,
# Get coordmap error message if present
error = attr(result$coordmap, "error", exact = TRUE)
@@ -305,6 +345,24 @@ custom_print.ggplot <- function(x) {
), class = "ggplot_build_gtable")
}
# Infer alt text description from renderPlot() value
# (currently just ggplot2 is supported)
getAltText <- function(x, default = "Plot object") {
# Since, inside renderPlot(), custom_print.ggplot()
# overrides print.ggplot, this class indicates a ggplot()
if (!inherits(x, "ggplot_build_gtable")) {
return(default)
}
# ggplot2::get_alt_text() was added in v3.3.4
# https://github.com/tidyverse/ggplot2/pull/4482
get_alt <- getNamespace("ggplot2")$get_alt_text
if (!is.function(get_alt)) {
return(default)
}
alt <- paste(get_alt(x$build), collapse = " ")
if (nzchar(alt)) alt else default
}
# The coordmap extraction functions below return something like the examples
# below. For base graphics:
# plot(mtcars$wt, mtcars$mpg)
@@ -556,7 +614,7 @@ getGgplotCoordmap <- function(p, width, height, res) {
find_panel_info <- function(b) {
# Structure of ggplot objects changed after 2.1.0. After 2.2.1, there was a
# an API for extracting the necessary information.
ggplot_ver <- utils::packageVersion("ggplot2")
ggplot_ver <- get_package_version("ggplot2")
if (ggplot_ver > "2.2.1") {
find_panel_info_api(b)
@@ -576,6 +634,10 @@ find_panel_info_api <- function(b) {
coord <- ggplot2::summarise_coord(b)
layers <- ggplot2::summarise_layers(b)
`%NA_OR%` <- function(x, y) {
if (is_na(x)) y else x
}
# Given x and y scale objects and a coord object, return a list that has
# the bases of log transformations for x and y, or NULL if it's not a
# log transform.
@@ -592,8 +654,8 @@ find_panel_info_api <- function(b) {
# First look for log base in scale, then coord; otherwise NULL.
list(
x = get_log_base(xscale$trans) %OR% coord$xlog %OR% NULL,
y = get_log_base(yscale$trans) %OR% coord$ylog %OR% NULL
x = get_log_base(xscale$trans) %NA_OR% coord$xlog %NA_OR% NULL,
y = get_log_base(yscale$trans) %NA_OR% coord$ylog %NA_OR% NULL
)
}

View File

@@ -1,10 +1,12 @@
#' Table Output
#'
#' Creates a reactive table that is suitable for assigning to an `output`
#' slot.
#' @description
#' The `tableOuptut()`/`renderTable()` pair creates a reactive table that is
#' suitable for display small matrices and data frames. The columns are
#' formatted with [xtable::xtable()].
#'
#' The corresponding HTML output tag should be `div` and have the CSS
#' class name `shiny-html-output`.
#' See [renderDataTable()] for data frames that are too big to fit on a single
#' page.
#'
#' @param expr An expression that returns an R object that can be used with
#' [xtable::xtable()].
@@ -40,21 +42,37 @@
#' (i.e. they either evaluate to `NA` or `NaN`).
#' @param ... Arguments to be passed through to [xtable::xtable()]
#' and [xtable::print.xtable()].
#' @param env The environment in which to evaluate `expr`.
#' @param quoted Is `expr` a quoted expression (with `quote()`)?
#' This is useful if you want to save an expression in a variable.
#' @inheritParams renderUI
#' @param outputArgs A list of arguments to be passed through to the
#' implicit call to [tableOutput()] when `renderTable` is
#' used in an interactive R Markdown document.
#' @export
#' @examples
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#' # table example
#' shinyApp(
#' ui = fluidPage(
#' fluidRow(
#' column(12,
#' tableOutput('table')
#' )
#' )
#' ),
#' server = function(input, output) {
#' output$table <- renderTable(iris)
#' }
#' )
#' }
renderTable <- function(expr, striped = FALSE, hover = FALSE,
bordered = FALSE, spacing = c("s", "xs", "m", "l"),
width = "auto", align = NULL,
rownames = FALSE, colnames = TRUE,
digits = NULL, na = "NA", ...,
env = parent.frame(), quoted = FALSE,
outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
outputArgs=list())
{
func <- installExprFunction(expr, "func", env, quoted, label = "renderTable")
if (!is.function(spacing)) spacing <- match.arg(spacing)

View File

@@ -23,10 +23,10 @@
#' @examples
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#' runUrl('https://github.com/rstudio/shiny_example/archive/master.tar.gz')
#' runUrl('https://github.com/rstudio/shiny_example/archive/main.tar.gz')
#'
#' # Can run an app from a subdirectory in the archive
#' runUrl("https://github.com/rstudio/shiny_example/archive/master.zip",
#' runUrl("https://github.com/rstudio/shiny_example/archive/main.zip",
#' subdir = "inst/shinyapp/")
#' }
runUrl <- function(url, filetype = NULL, subdir = NULL, destdir = NULL, ...) {
@@ -121,7 +121,8 @@ runGist <- function(gist, destdir = NULL, ...) {
#' @param username GitHub username. If `repo` is of the form
#' `"username/repo"`, `username` will be taken from `repo`.
#' @param ref Desired git reference. Could be a commit, tag, or branch name.
#' Defaults to `"master"`.
#' Defaults to `"HEAD"`, which means the default branch on GitHub, typically
#' `"main"` or `"master"`.
#' @export
#' @examples
#' ## Only run this example in interactive R sessions
@@ -133,7 +134,7 @@ runGist <- function(gist, destdir = NULL, ...) {
#' runGitHub("shiny_example", "rstudio", subdir = "inst/shinyapp/")
#' }
runGitHub <- function(repo, username = getOption("github.user"),
ref = "master", subdir = NULL, destdir = NULL, ...) {
ref = "HEAD", subdir = NULL, destdir = NULL, ...) {
if (grepl('/', repo)) {
res <- strsplit(repo, '/')[[1]]

601
R/runapp.R Normal file
View File

@@ -0,0 +1,601 @@
#' Run Shiny Application
#'
#' Runs a Shiny application. This function normally does not return; interrupt R
#' to stop the application (usually by pressing Ctrl+C or Esc).
#'
#' The host parameter was introduced in Shiny 0.9.0. Its default value of
#' `"127.0.0.1"` means that, contrary to previous versions of Shiny, only
#' the current machine can access locally hosted Shiny apps. To allow other
#' clients to connect, use the value `"0.0.0.0"` instead (which was the
#' value that was hard-coded into Shiny in 0.8.0 and earlier).
#'
#' @param appDir The application to run. Should be one of the following:
#' \itemize{
#' \item A directory containing `server.R`, plus, either `ui.R` or
#' a `www` directory that contains the file `index.html`.
#' \item A directory containing `app.R`.
#' \item An `.R` file containing a Shiny application, ending with an
#' expression that produces a Shiny app object.
#' \item A list with `ui` and `server` components.
#' \item A Shiny app object created by [shinyApp()].
#' }
#' @param port The TCP port that the application should listen on. If the
#' `port` is not specified, and the `shiny.port` option is set (with
#' `options(shiny.port = XX)`), then that port will be used. Otherwise,
#' use a random port between 3000:8000, excluding ports that are blocked
#' by Google Chrome for being considered unsafe: 3659, 4045, 5060,
#' 5061, 6000, 6566, 6665:6669 and 6697. Up to twenty random
#' ports will be tried.
#' @param launch.browser If true, the system's default web browser will be
#' launched automatically after the app is started. Defaults to true in
#' interactive sessions only. The value of this parameter can also be a
#' function to call with the application's URL.
#' @param host The IPv4 address that the application should listen on. Defaults
#' to the `shiny.host` option, if set, or `"127.0.0.1"` if not. See
#' Details.
#' @param workerId Can generally be ignored. Exists to help some editions of
#' Shiny Server Pro route requests to the correct process.
#' @param quiet Should Shiny status messages be shown? Defaults to FALSE.
#' @param display.mode The mode in which to display the application. If set to
#' the value `"showcase"`, shows application code and metadata from a
#' `DESCRIPTION` file in the application directory alongside the
#' application. If set to `"normal"`, displays the application normally.
#' Defaults to `"auto"`, which displays the application in the mode given
#' in its `DESCRIPTION` file, if any.
#' @param test.mode Should the application be launched in test mode? This is
#' only used for recording or running automated tests. Defaults to the
#' `shiny.testmode` option, or FALSE if the option is not set.
#'
#' @examples
#' \dontrun{
#' # Start app in the current working directory
#' runApp()
#'
#' # Start app in a subdirectory called myapp
#' runApp("myapp")
#' }
#'
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#' options(device.ask.default = FALSE)
#'
#' # Apps can be run without a server.r and ui.r file
#' runApp(list(
#' ui = bootstrapPage(
#' numericInput('n', 'Number of obs', 100),
#' plotOutput('plot')
#' ),
#' server = function(input, output) {
#' output$plot <- renderPlot({ hist(runif(input$n)) })
#' }
#' ))
#'
#'
#' # Running a Shiny app object
#' app <- shinyApp(
#' ui = bootstrapPage(
#' numericInput('n', 'Number of obs', 100),
#' plotOutput('plot')
#' ),
#' server = function(input, output) {
#' output$plot <- renderPlot({ hist(runif(input$n)) })
#' }
#' )
#' runApp(app)
#' }
#' @export
runApp <- function(
appDir=getwd(),
port=getOption('shiny.port'),
launch.browser = getOption('shiny.launch.browser', interactive()),
host=getOption('shiny.host', '127.0.0.1'),
workerId="", quiet=FALSE,
display.mode=c("auto", "normal", "showcase"),
test.mode=getOption('shiny.testmode', FALSE)
) {
# * Wrap **all** execution of the app inside the otel promise domain
# * While this could be done at a lower level, it allows for _anything_ within
# shiny's control to allow for the opportunity to have otel active spans be
# reactivated upon promise domain restoration
promises::local_otel_promise_domain()
on.exit({
handlerManager$clear()
}, add = TRUE)
if (isRunning()) {
stop("Can't call `runApp()` from within `runApp()`. If your ",
"application code contains `runApp()`, please remove it.")
}
# Make warnings print immediately
# Set pool.scheduler to support pool package
ops <- options(
# Raise warn level to 1, but don't lower it
warn = max(1, getOption("warn", default = 1)),
pool.scheduler = scheduleTask
)
on.exit(options(ops), add = TRUE)
# ============================================================================
# Global onStart/onStop callbacks
# ============================================================================
# Invoke user-defined onStop callbacks, before the application's internal
# onStop callbacks.
on.exit({
.globals$onStopCallbacks$invoke()
.globals$onStopCallbacks <- Callbacks$new()
}, add = TRUE)
require(shiny)
# ============================================================================
# Convert to Shiny app object
# ============================================================================
appParts <- as.shiny.appobj(appDir)
# ============================================================================
# Initialize app state object
# ============================================================================
# This is so calls to getCurrentAppState() can be used to find (A) whether an
# app is running and (B), get options and data associated with the app.
initCurrentAppState(appParts)
on.exit(clearCurrentAppState(), add = TRUE)
# Any shinyOptions set after this point will apply to the current app only
# (and will not persist after the app stops).
# ============================================================================
# shinyOptions
# ============================================================================
# A unique identifier associated with this run of this application. It is
# shared across sessions.
shinyOptions(appToken = createUniqueId(8))
# Set up default cache for app.
if (is.null(getShinyOption("cache", default = NULL))) {
shinyOptions(cache = cachem::cache_mem(max_size = 200 * 1024^2))
}
# Extract appOptions (which is a list) and store them as shinyOptions, for
# this app. (This is the only place we have to store settings that are
# accessible both the UI and server portion of the app.)
applyCapturedAppOptions(appParts$appOptions)
# ============================================================================
# runApp options set via shinyApp(options = list(...))
# ============================================================================
# The lines below set some of the app's running options, which
# can be:
# - left unspecified (in which case the arguments' default
# values from `runApp` kick in);
# - passed through `shinyApp`
# - passed through `runApp` (this function)
# - passed through both `shinyApp` and `runApp` (the latter
# takes precedence)
#
# Matrix of possibilities:
# | IN shinyApp | IN runApp | result | check |
# |-------------|-----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------|
# | no | no | use defaults | exhaust all possibilities: if it's missing (runApp does not specify); THEN if it's not in shinyApp appParts$options; THEN use defaults |
# | yes | no | use shinyApp | if it's missing (runApp does not specify); THEN if it's in shinyApp appParts$options; THEN use shinyApp |
# | no | yes | use runApp | if it's not missing (runApp specifies), use those |
# | yes | yes | use runApp | if it's not missing (runApp specifies), use those |
#
# I tried to make this as compact and intuitive as possible,
# given that there are four distinct possibilities to check
appOps <- appParts$options
findVal <- function(arg, default) {
if (arg %in% names(appOps)) appOps[[arg]] else default
}
if (missing(port))
port <- findVal("port", port)
if (missing(launch.browser))
launch.browser <- findVal("launch.browser", launch.browser)
if (missing(host))
host <- findVal("host", host)
if (missing(quiet))
quiet <- findVal("quiet", quiet)
if (missing(display.mode))
display.mode <- findVal("display.mode", display.mode)
if (missing(test.mode))
test.mode <- findVal("test.mode", test.mode)
if (is.null(host) || is.na(host)) host <- '0.0.0.0'
# ============================================================================
# Hosted environment
# ============================================================================
workerId(workerId)
if (inShinyServer()) {
# If SHINY_PORT is set, we're running under Shiny Server. Check the version
# to make sure it is compatible. Older versions of Shiny Server don't set
# SHINY_SERVER_VERSION, those will return "" which is considered less than
# any valid version.
ver <- Sys.getenv('SHINY_SERVER_VERSION')
if (utils::compareVersion(ver, .shinyServerMinVersion) < 0) {
warning('Shiny Server v', .shinyServerMinVersion,
' or later is required; please upgrade!')
}
}
# ============================================================================
# Shinytest
# ============================================================================
# Set the testmode shinyoption so that this can be read by both the
# ShinySession and the UI code (which executes separately from the
# ShinySession code).
shinyOptions(testmode = test.mode)
if (test.mode) {
message("Running application in test mode.")
}
# ============================================================================
# Showcase mode
# ============================================================================
# Showcase mode is disabled by default; it must be explicitly enabled in
# either the DESCRIPTION file for directory-based apps, or via
# the display.mode parameter. The latter takes precedence.
setShowcaseDefault(0)
# If appDir specifies a path, and display mode is specified in the
# DESCRIPTION file at that path, apply it here.
if (is.character(appDir)) {
# if appDir specifies a .R file (single-file Shiny app), look for the
# DESCRIPTION in the parent directory
desc <- file.path.ci(
if (tolower(tools::file_ext(appDir)) == "r")
dirname(appDir)
else
appDir, "DESCRIPTION")
if (file.exists(desc)) {
con <- file(desc, encoding = checkEncoding(desc))
on.exit(close(con), add = TRUE)
settings <- read.dcf(con)
if ("DisplayMode" %in% colnames(settings)) {
mode <- settings[1, "DisplayMode"]
if (mode == "Showcase") {
setShowcaseDefault(1)
if ("IncludeWWW" %in% colnames(settings)) {
.globals$IncludeWWW <- as.logical(settings[1, "IncludeWWW"])
if (is.na(.globals$IncludeWWW)) {
stop("In your Description file, `IncludeWWW` ",
"must be set to `True` (default) or `False`")
}
} else {
.globals$IncludeWWW <- TRUE
}
}
}
}
}
## default is to show the .js, .css and .html files in the www directory
## (if not in showcase mode, this variable will simply be ignored)
if (is.null(.globals$IncludeWWW) || is.na(.globals$IncludeWWW)) {
.globals$IncludeWWW <- TRUE
}
# If display mode is specified as an argument, apply it (overriding the
# value specified in DESCRIPTION, if any).
display.mode <- match.arg(display.mode)
if (display.mode == "normal") {
setShowcaseDefault(0)
}
else if (display.mode == "showcase") {
setShowcaseDefault(1)
}
# ============================================================================
# Server port
# ============================================================================
# determine port if we need to
if (is.null(port)) {
# Try up to 20 random ports. If we don't succeed just plow ahead
# with the final value we tried, and let the "real" startServer
# somewhere down the line fail and throw the error to the user.
#
# If we (think we) succeed, save the value as .globals$lastPort,
# and try that first next time the user wants a random port.
for (i in 1:20) {
if (!is.null(.globals$lastPort)) {
port <- .globals$lastPort
.globals$lastPort <- NULL
}
else {
# Try up to 20 random ports
while (TRUE) {
port <- p_randomInt(3000, 8000)
# Reject ports in this range that are considered unsafe by Chrome
# http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
# https://github.com/rstudio/shiny/issues/1784
# https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/net/base/port_util.cc
if (!port %in% c(3659, 4045, 5060, 5061, 6000, 6566, 6665:6669, 6697)) {
break
}
}
}
# Test port to see if we can use it
tmp <- try(startServer(host, port, list()), silent=TRUE)
if (!inherits(tmp, 'try-error')) {
stopServer(tmp)
.globals$lastPort <- port
break
}
}
}
# ============================================================================
# onStart/onStop callbacks
# ============================================================================
# Set up the onStop before we call onStart, so that it gets called even if an
# error happens in onStart.
if (!is.null(appParts$onStop))
on.exit(appParts$onStop(), add = TRUE)
if (!is.null(appParts$onStart))
appParts$onStart()
# ============================================================================
# Start/stop httpuv app
# ============================================================================
server <- startApp(appParts, port, host, quiet)
# Make the httpuv server object accessible. Needed for calling
# addResourcePath while app is running.
shinyOptions(server = server)
on.exit({
stopServer(server)
}, add = TRUE)
# ============================================================================
# Launch web browser
# ============================================================================
if (!is.character(port)) {
browseHost <- host
if (identical(host, "0.0.0.0")) {
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
browseHost <- "127.0.0.1"
} else if (identical(host, "::")) {
browseHost <- "::1"
}
if (httpuv::ipFamily(browseHost) == 6L) {
browseHost <- paste0("[", browseHost, "]")
}
appUrl <- paste("http://", browseHost, ":", port, sep="")
if (is.function(launch.browser))
launch.browser(appUrl)
else if (launch.browser)
utils::browseURL(appUrl)
} else {
appUrl <- NULL
}
# ============================================================================
# Application hooks
# ============================================================================
callAppHook("onAppStart", appUrl)
on.exit({
callAppHook("onAppStop", appUrl)
}, add = TRUE)
# ============================================================================
# Run event loop via httpuv
# ============================================================================
.globals$reterror <- NULL
.globals$retval <- NULL
.globals$stopped <- FALSE
# Top-level ..stacktraceoff..; matches with ..stacktraceon in observe(),
# reactive(), Callbacks$invoke(), and others
..stacktraceoff..(
captureStackTraces({
while (!.globals$stopped) {
..stacktracefloor..(serviceApp())
}
})
)
if (isTRUE(.globals$reterror)) {
stop(.globals$retval)
}
else if (.globals$retval$visible)
.globals$retval$value
else
invisible(.globals$retval$value)
}
#' Stop the currently running Shiny app
#'
#' Stops the currently running Shiny app, returning control to the caller of
#' [runApp()].
#'
#' @param returnValue The value that should be returned from
#' [runApp()].
#' @export
stopApp <- function(returnValue = invisible()) {
# reterror will indicate whether retval is an error (i.e. it should be passed
# to stop() when the serviceApp loop stops) or a regular value (in which case
# it should simply be returned with the appropriate visibility).
.globals$reterror <- FALSE
..stacktraceoff..(
tryCatch(
{
captureStackTraces(
.globals$retval <- withVisible(..stacktraceon..(force(returnValue)))
)
},
error = function(e) {
.globals$retval <- e
.globals$reterror <- TRUE
}
)
)
.globals$stopped <- TRUE
httpuv::interrupt()
}
#' Run Shiny Example Applications
#'
#' Launch Shiny example applications, and optionally, your system's web browser.
#'
#' @param example The name of the example to run, or `NA` (the default) to
#' list the available examples.
#' @param launch.browser If true, the system's default web browser will be
#' launched automatically after the app is started. Defaults to true in
#' interactive sessions only.
#' @param host The IPv4 address that the application should listen on. Defaults
#' to the `shiny.host` option, if set, or `"127.0.0.1"` if not.
#' @param display.mode The mode in which to display the example. Defaults to
#' `"auto"`, which uses the value of `DisplayMode` in the example's
#' `DESCRIPTION` file. Set to `"showcase"` to show the app code and
#' description with the running app, or `"normal"` to see the example without
#' code or commentary.
#' @param package The package in which to find the example (defaults to
#' `"shiny"`).
#'
#' To provide examples in your package, store examples in the
#' `inst/examples-shiny` directory of your package. Each example should be
#' in its own subdirectory and should be runnable when [runApp()] is called
#' on the subdirectory. Example apps can include a `DESCRIPTION` file and a
#' `README.md` file to provide metadata and commentary about the example. See
#' the article on [Display Modes](https://shiny.posit.co/r/articles/build/display-modes/)
#' on the Shiny website for more information.
#' @inheritParams runApp
#'
#' @examples
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#' # List all available examples
#' runExample()
#'
#' # Run one of the examples
#' runExample("01_hello")
#'
#' # Print the directory containing the code for all examples
#' system.file("examples", package="shiny")
#' }
#' @export
runExample <- function(
example = NA,
port = getOption("shiny.port"),
launch.browser = getOption("shiny.launch.browser", interactive()),
host = getOption("shiny.host", "127.0.0.1"),
display.mode = c("auto", "normal", "showcase"),
package = "shiny"
) {
if (!identical(package, "shiny") && !is_installed(package)) {
rlang::check_installed(package)
}
use_legacy_shiny_examples <-
identical(package, "shiny") &&
isTRUE(getOption('shiny.legacy.examples', FALSE))
examplesDir <- system_file(
if (use_legacy_shiny_examples) "examples" else "examples-shiny",
package = package
)
dir <- resolve(examplesDir, example)
if (is.null(dir)) {
valid_examples <- sprintf(
'Valid examples in {%s}: "%s"',
package,
paste(list.files(examplesDir), collapse = '", "')
)
if (is.na(example)) {
message(valid_examples)
return(invisible())
}
stop("Example '", example, "' does not exist. ", valid_examples)
}
runApp(dir, port = port, host = host, launch.browser = launch.browser,
display.mode = display.mode)
}
#' Run a gadget
#'
#' Similar to `runApp`, but handles `input$cancel` automatically, and
#' if running in RStudio, defaults to viewing the app in the Viewer pane.
#'
#' @param app Either a Shiny app object as created by
#' [`shinyApp()`][shiny] et al, or, a UI object.
#' @param server Ignored if `app` is a Shiny app object; otherwise, passed
#' along to `shinyApp` (i.e. `shinyApp(ui = app, server = server)`).
#' @param port See [`runApp()`][shiny].
#' @param viewer Specify where the gadget should be displayed--viewer pane,
#' dialog window, or external browser--by passing in a call to one of the
#' [viewer()] functions.
#' @param stopOnCancel If `TRUE` (the default), then an `observeEvent`
#' is automatically created that handles `input$cancel` by calling
#' `stopApp()` with an error. Pass `FALSE` if you want to handle
#' `input$cancel` yourself.
#' @return The value returned by the gadget.
#'
#' @examples
#' \dontrun{
#' library(shiny)
#'
#' ui <- fillPage(...)
#'
#' server <- function(input, output, session) {
#' ...
#' }
#'
#' # Either pass ui/server as separate arguments...
#' runGadget(ui, server)
#'
#' # ...or as a single app object
#' runGadget(shinyApp(ui, server))
#' }
#' @export
runGadget <- function(app, server = NULL, port = getOption("shiny.port"),
viewer = paneViewer(), stopOnCancel = TRUE) {
if (!is.shiny.appobj(app)) {
app <- shinyApp(app, server)
}
if (isTRUE(stopOnCancel)) {
app <- decorateServerFunc(app, function(input, output, session) {
observeEvent(input$cancel, {
stopApp(stop("User cancel", call. = FALSE))
})
})
}
if (is.null(viewer)) {
viewer <- utils::browseURL
}
shiny::runApp(app, port = port, launch.browser = viewer)
}
# Add custom functionality to a Shiny app object's server func
decorateServerFunc <- function(appobj, serverFunc) {
origServerFuncSource <- appobj$serverFuncSource
appobj$serverFuncSource <- function() {
origServerFunc <- origServerFuncSource()
function(input, output, session) {
serverFunc(input, output, session)
# The clientData and session arguments are optional; check if
# each exists
args <- argsForServerFunc(origServerFunc, session)
do.call(origServerFunc, args)
}
}
appobj
}

View File

@@ -5,7 +5,6 @@
#' value. The returned value will be used for the test snapshot.
#' @param session A Shiny session object.
#'
#' @keywords internal
#' @export
setSerializer <- function(inputId, fun, session = getDefaultReactiveDomain()) {
if (is.null(session)) {

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
#'
@@ -41,12 +45,12 @@ inputHandlers <- Map$new()
#' })
#'
#' ## On the Javascript side, the associated input binding must have a corresponding getType method:
#' getType: function(el) {
#' return "mypackage.validint";
#' }
#' # getType: function(el) {
#' # return "mypackage.validint";
#' # }
#'
#' }
#' @seealso [removeInputHandler()]
#' @seealso [removeInputHandler()] [applyInputHandlers()]
#' @export
registerInputHandler <- function(type, fun, force=FALSE){
if (inputHandlers$containsKey(type) && !force){
@@ -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(class(val), "shinyActionButtonValue")
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)
}
})
})

169
R/server-resource-paths.R Normal file
View File

@@ -0,0 +1,169 @@
.globals$resourcePaths <- list()
.globals$resources <- list()
#' Resource Publishing
#'
#' Add, remove, or list directory of static resources to Shiny's web server,
#' with the given path prefix. Primarily intended for package authors to make
#' supporting JavaScript/CSS files available to their components.
#'
#' Shiny provides two ways of serving static files (i.e., resources):
#'
#' 1. Static files under the `www/` directory are automatically made available
#' under a request path that begins with `/`.
#' 2. `addResourcePath()` makes static files in a `directoryPath` available
#' under a request path that begins with `prefix`.
#'
#' The second approach is primarily intended for package authors to make
#' supporting JavaScript/CSS files available to their components.
#'
#' Tools for managing static resources published by Shiny's web server:
#' * `addResourcePath()` adds a directory of static resources.
#' * `resourcePaths()` lists the currently active resource mappings.
#' * `removeResourcePath()` removes a directory of static resources.
#'
#' @param prefix The URL prefix (without slashes). Valid characters are a-z,
#' A-Z, 0-9, hyphen, period, and underscore. For example, a value of 'foo'
#' means that any request paths that begin with '/foo' will be mapped to the
#' given directory.
#' @param directoryPath The directory that contains the static resources to be
#' served.
#'
#' @rdname resourcePaths
#' @seealso [singleton()]
#'
#' @examples
#' addResourcePath('datasets', system.file('data', package='datasets'))
#' resourcePaths()
#' removeResourcePath('datasets')
#' resourcePaths()
#'
#' # make sure all resources are removed
#' lapply(names(resourcePaths()), removeResourcePath)
#' @export
addResourcePath <- function(prefix, directoryPath) {
if (length(prefix) != 1) stop("prefix must be of length 1")
if (grepl("^\\.+$", prefix)) stop("prefix can't be composed of dots only")
if (!grepl('[a-z0-9\\-_.]+$', prefix, ignore.case = TRUE, perl = TRUE)) {
stop("addResourcePath called with invalid prefix; please see documentation")
}
if (prefix %in% c('shared')) {
stop("addResourcePath called with the reserved prefix '", prefix, "'; ",
"please use a different prefix")
}
normalizedPath <- tryCatch(normalizePath(directoryPath, mustWork = TRUE),
error = function(e) {
stop("Couldn't normalize path in `addResourcePath`, with arguments: ",
"`prefix` = '", prefix, "'; `directoryPath` = '" , directoryPath, "'")
}
)
# # Often times overwriting a resource path is "what you want",
# # but sometimes it can lead to difficult to diagnose issues
# # (e.g. an implict dependency might set a resource path that
# # conflicts with what you, the app author, are trying to register)
# # Note that previous versions of shiny used to warn about this case,
# # but it was eventually removed since it caused confusion (#567).
# # It seems a good compromise is to throw a more information message.
# if (getOption("shiny.resourcePathChanges", FALSE) &&
# prefix %in% names(.globals$resourcePaths)) {
# existingPath <- .globals$resourcePaths[[prefix]]$path
# if (normalizedPath != existingPath) {
# message(
# "The resource path '", prefix, "' used to point to ",
# existingPath, ", but it now points to ", normalizedPath, ". ",
# "If your app doesn't work as expected, you may want to ",
# "choose a different prefix name."
# )
# }
# }
# If a shiny app is currently running, dynamically register this path with
# the corresponding httpuv server object.
if (!is.null(getShinyOption("server", default = NULL)))
{
getShinyOption("server")$setStaticPath(.list = stats::setNames(normalizedPath, prefix))
}
# .globals$resourcePaths and .globals$resources persist across runs of applications.
.globals$resourcePaths[[prefix]] <- staticPath(normalizedPath)
# This is necessary because resourcePaths is only for serving assets out of C++;
# to support subapps, we also need assets to be served out of R, because those
# URLs are rewritten by R code (i.e. routeHandler) before they can be matched to
# a resource path.
.globals$resources[[prefix]] <- list(
directoryPath = normalizedPath,
func = staticHandler(normalizedPath)
)
}
#' @rdname resourcePaths
#' @export
resourcePaths <- function() {
urls <- names(.globals$resourcePaths)
paths <- vapply(.globals$resourcePaths, function(x) x$path, character(1))
stats::setNames(paths, urls)
}
hasResourcePath <- function(prefix) {
prefix %in% names(resourcePaths())
}
#' @rdname resourcePaths
#' @export
removeResourcePath <- function(prefix) {
if (length(prefix) > 1) stop("`prefix` must be of length 1.")
if (!hasResourcePath(prefix)) {
warning("Resource ", prefix, " not found.")
return(invisible(FALSE))
}
.globals$resourcePaths[[prefix]] <- NULL
.globals$resources[[prefix]] <- NULL
invisible(TRUE)
}
# This function handles any GET request with two or more path elements where the
# first path element matches a prefix that was previously added using
# addResourcePath().
#
# For example, if `addResourcePath("foo", "~/bar")` was called, then a GET
# request for /foo/one/two.html would rewrite the PATH_INFO as /one/two.html and
# send it to the resource path function for "foo". As of this writing, that
# function will always be a staticHandler, which serves up a file if it exists
# and NULL if it does not.
#
# Since Shiny 1.3.x, assets registered via addResourcePath should mostly be
# served out of httpuv's native static file serving features. However, in the
# specific case of subapps, the R code path must be used, because subapps insert
# a giant random ID into the beginning of the URL that must be stripped off by
# an R route handler (see addSubApp()).
resourcePathHandler <- function(req) {
if (!identical(req$REQUEST_METHOD, 'GET'))
return(NULL)
# e.g. "/foo/one/two.html"
path <- req$PATH_INFO
match <- regexpr('^/([^/]+)/', path, perl=TRUE)
if (match == -1)
return(NULL)
len <- attr(match, 'capture.length')
# e.g. "foo"
prefix <- substr(path, 2, 2 + len - 1)
resInfo <- .globals$resources[[prefix]]
if (is.null(resInfo))
return(NULL)
# e.g. "/one/two.html"
suffix <- substr(path, 2 + len, nchar(path))
# Create a new request that's a clone of the current request, but adjust
# PATH_INFO and SCRIPT_NAME to reflect that we have already matched the first
# path element (e.g. "/foo"). See routeHandler() for more info.
subreq <- as.environment(as.list(req, all.names=TRUE))
subreq$PATH_INFO <- suffix
subreq$SCRIPT_NAME <- paste(subreq$SCRIPT_NAME, substr(path, 1, 2 + len), sep='')
return(resInfo$func(subreq))
}

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.
@@ -22,185 +27,16 @@ registerClient <- function(client) {
}
.globals$resourcePaths <- list()
.globals$resources <- list()
.globals$showcaseDefault <- 0
.globals$showcaseOverride <- FALSE
#' Resource Publishing
#'
#' Add, remove, or list directory of static resources to Shiny's web server,
#' with the given path prefix. Primarily intended for package authors to make
#' supporting JavaScript/CSS files available to their components.
#'
#' Shiny provides two ways of serving static files (i.e., resources):
#'
#' 1. Static files under the `www/` directory are automatically made available
#' under a request path that begins with `/`.
#' 2. `addResourcePath()` makes static files in a `directoryPath` available
#' under a request path that begins with `prefix`.
#'
#' The second approach is primarily intended for package authors to make
#' supporting JavaScript/CSS files available to their components.
#'
#' Tools for managing static resources published by Shiny's web server:
#' * `addResourcePath()` adds a directory of static resources.
#' * `resourcePaths()` lists the currently active resource mappings.
#' * `removeResourcePath()` removes a directory of static resources.
#'
#' @param prefix The URL prefix (without slashes). Valid characters are a-z,
#' A-Z, 0-9, hyphen, period, and underscore. For example, a value of 'foo'
#' means that any request paths that begin with '/foo' will be mapped to the
#' given directory.
#' @param directoryPath The directory that contains the static resources to be
#' served.
#'
#' @rdname resourcePaths
#' @seealso [singleton()]
#'
#' @examples
#' addResourcePath('datasets', system.file('data', package='datasets'))
#' resourcePaths()
#' removeResourcePath('datasets')
#' resourcePaths()
#'
#' # make sure all resources are removed
#' lapply(names(resourcePaths()), removeResourcePath)
#' @export
addResourcePath <- function(prefix, directoryPath) {
if (length(prefix) != 1) stop("prefix must be of length 1")
if (grepl("^\\.+$", prefix)) stop("prefix can't be composed of dots only")
if (!grepl('[a-z0-9\\-_.]+$', prefix, ignore.case = TRUE, perl = TRUE)) {
stop("addResourcePath called with invalid prefix; please see documentation")
}
if (prefix %in% c('shared')) {
stop("addResourcePath called with the reserved prefix '", prefix, "'; ",
"please use a different prefix")
}
normalizedPath <- tryCatch(normalizePath(directoryPath, mustWork = TRUE),
error = function(e) {
stop("Couldn't normalize path in `addResourcePath`, with arguments: ",
"`prefix` = '", prefix, "'; `directoryPath` = '" , directoryPath, "'")
}
)
# # Often times overwriting a resource path is "what you want",
# # but sometimes it can lead to difficult to diagnose issues
# # (e.g. an implict dependency might set a resource path that
# # conflicts with what you, the app author, are trying to register)
# # Note that previous versions of shiny used to warn about this case,
# # but it was eventually removed since it caused confusion (#567).
# # It seems a good compromise is to throw a more information message.
# if (getOption("shiny.resourcePathChanges", FALSE) &&
# prefix %in% names(.globals$resourcePaths)) {
# existingPath <- .globals$resourcePaths[[prefix]]$path
# if (normalizedPath != existingPath) {
# message(
# "The resource path '", prefix, "' used to point to ",
# existingPath, ", but it now points to ", normalizedPath, ". ",
# "If your app doesn't work as expected, you may want to ",
# "choose a different prefix name."
# )
# }
# }
# If a shiny app is currently running, dynamically register this path with
# the corresponding httpuv server object.
if (!is.null(getShinyOption("server")))
{
getShinyOption("server")$setStaticPath(.list = stats::setNames(normalizedPath, prefix))
}
# .globals$resourcePaths and .globals$resources persist across runs of applications.
.globals$resourcePaths[[prefix]] <- staticPath(normalizedPath)
# This is necessary because resourcePaths is only for serving assets out of C++;
# to support subapps, we also need assets to be served out of R, because those
# URLs are rewritten by R code (i.e. routeHandler) before they can be matched to
# a resource path.
.globals$resources[[prefix]] <- list(
directoryPath = normalizedPath,
func = staticHandler(normalizedPath)
)
}
#' @rdname resourcePaths
#' @export
resourcePaths <- function() {
urls <- names(.globals$resourcePaths)
paths <- vapply(.globals$resourcePaths, function(x) x$path, character(1))
stats::setNames(paths, urls)
}
hasResourcePath <- function(prefix) {
prefix %in% names(resourcePaths())
}
#' @rdname resourcePaths
#' @export
removeResourcePath <- function(prefix) {
if (length(prefix) > 1) stop("`prefix` must be of length 1.")
if (!hasResourcePath(prefix)) {
warning("Resource ", prefix, " not found.")
return(invisible(FALSE))
}
.globals$resourcePaths[[prefix]] <- NULL
.globals$resources[[prefix]] <- NULL
invisible(TRUE)
}
# This function handles any GET request with two or more path elements where the
# first path element matches a prefix that was previously added using
# addResourcePath().
#
# For example, if `addResourcePath("foo", "~/bar")` was called, then a GET
# request for /foo/one/two.html would rewrite the PATH_INFO as /one/two.html and
# send it to the resource path function for "foo". As of this writing, that
# function will always be a staticHandler, which serves up a file if it exists
# and NULL if it does not.
#
# Since Shiny 1.3.x, assets registered via addResourcePath should mostly be
# served out of httpuv's native static file serving features. However, in the
# specific case of subapps, the R code path must be used, because subapps insert
# a giant random ID into the beginning of the URL that must be stripped off by
# an R route handler (see addSubApp()).
resourcePathHandler <- function(req) {
if (!identical(req$REQUEST_METHOD, 'GET'))
return(NULL)
# e.g. "/foo/one/two.html"
path <- req$PATH_INFO
match <- regexpr('^/([^/]+)/', path, perl=TRUE)
if (match == -1)
return(NULL)
len <- attr(match, 'capture.length')
# e.g. "foo"
prefix <- substr(path, 2, 2 + len - 1)
resInfo <- .globals$resources[[prefix]]
if (is.null(resInfo))
return(NULL)
# e.g. "/one/two.html"
suffix <- substr(path, 2 + len, nchar(path))
# Create a new request that's a clone of the current request, but adjust
# PATH_INFO and SCRIPT_NAME to reflect that we have already matched the first
# path element (e.g. "/foo"). See routeHandler() for more info.
subreq <- as.environment(as.list(req, all.names=TRUE))
subreq$PATH_INFO <- suffix
subreq$SCRIPT_NAME <- paste(subreq$SCRIPT_NAME, substr(path, 1, 2 + len), sep='')
return(resInfo$func(subreq))
}
#' Define Server Functionality
#'
#' Defines the server-side logic of the Shiny application. This generally
#' @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.
#' In older versions of Shiny, it was necessary to call `shinyServer()` in
#' the `server.R` file, but this is no longer required as of Shiny 0.10.
@@ -218,7 +54,7 @@ resourcePathHandler <- function(req) {
#' optional `session` parameter, which is used when greater control is
#' needed.
#'
#' See the [tutorial](http://rstudio.github.com/shiny/tutorial/) for more
#' See the [tutorial](https://shiny.rstudio.com/tutorial/) for more
#' on how to write a server function.
#'
#' @param func The server function for this application. See the details section
@@ -247,6 +83,17 @@ resourcePathHandler <- function(req) {
#' @export
#' @keywords internal
shinyServer <- function(func) {
if (in_devmode()) {
shinyDeprecated(
"0.10.0", "shinyServer()",
details = paste0(
"When removing `shinyServer()`, ",
"ensure that the last expression returned from server.R ",
"is the function normally supplied to `shinyServer(func)`."
)
)
}
.globals$server <- list(func)
invisible(func)
}
@@ -280,13 +127,16 @@ decodeMessage <- function(data) {
return(mainMessage)
}
autoReloadCallbacks <- Callbacks$new()
autoReloadCallbacks <- NULL
on_load({
autoReloadCallbacks <- Callbacks$new()
})
createAppHandlers <- function(httpHandlers, serverFuncSource) {
appvars <- new.env()
appvars$server <- NULL
sys.www.root <- system.file('www', package='shiny')
sys.www.root <- system_file('www', package='shiny')
# This value, if non-NULL, must be present on all HTTP and WebSocket
# requests as the Shiny-Shared-Secret header or else access will be
@@ -308,7 +158,7 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
}
if (identical(ws$request$PATH_INFO, "/autoreload/")) {
if (!getOption("shiny.autoreload", FALSE)) {
if (!get_devmode_option("shiny.autoreload", FALSE)) {
ws$close()
return(TRUE)
}
@@ -424,15 +274,20 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
args <- argsForServerFunc(serverFunc, shinysession)
withReactiveDomain(shinysession, {
do.call(
# No corresponding ..stacktraceoff; the server func is pure
# user code
wrapFunctionLabel(appvars$server, "server",
..stacktraceon = TRUE
),
args
)
otel_span_session_start(domain = shinysession, {
do.call(
# No corresponding ..stacktraceoff; the server func is pure
# user code
wrapFunctionLabel(appvars$server, "server",
..stacktraceon = TRUE
),
args
)
})
})
})
},
update = {
@@ -489,7 +344,7 @@ argsForServerFunc <- function(serverFunc, session) {
getEffectiveBody <- function(func) {
if (is.null(func))
NULL
else if (isS4(func) && class(func) == "functionWithTrace")
else if (isS4(func) && inherits(func, "functionWithTrace"))
body(func@original)
else
body(func)
@@ -543,7 +398,7 @@ startApp <- function(appObj, port, host, quiet) {
list(
# Always handle /session URLs dynamically, even if / is a static path.
"session" = excludeStaticPath(),
"shared" = system.file(package = "shiny", "www", "shared")
"shared" = system_file(package = "shiny", "www", "shared")
),
.globals$resourcePaths
)
@@ -645,9 +500,6 @@ serviceApp <- function() {
.shinyServerMinVersion <- '0.3.4'
# Global flag that's TRUE whenever we're inside of the scope of a call to runApp
.globals$running <- FALSE
#' Check whether a Shiny application is running
#'
#' This function tests whether a Shiny application is currently running.
@@ -656,588 +508,9 @@ serviceApp <- function() {
#' `FALSE`.
#' @export
isRunning <- function() {
.globals$running
!is.null(getCurrentAppState())
}
#' Run Shiny Application
#'
#' Runs a Shiny application. This function normally does not return; interrupt R
#' to stop the application (usually by pressing Ctrl+C or Esc).
#'
#' The host parameter was introduced in Shiny 0.9.0. Its default value of
#' `"127.0.0.1"` means that, contrary to previous versions of Shiny, only
#' the current machine can access locally hosted Shiny apps. To allow other
#' clients to connect, use the value `"0.0.0.0"` instead (which was the
#' value that was hard-coded into Shiny in 0.8.0 and earlier).
#'
#' @param appDir The application to run. Should be one of the following:
#' \itemize{
#' \item A directory containing `server.R`, plus, either `ui.R` or
#' a `www` directory that contains the file `index.html`.
#' \item A directory containing `app.R`.
#' \item An `.R` file containing a Shiny application, ending with an
#' expression that produces a Shiny app object.
#' \item A list with `ui` and `server` components.
#' \item A Shiny app object created by [shinyApp()].
#' }
#' @param port The TCP port that the application should listen on. If the
#' `port` is not specified, and the `shiny.port` option is set (with
#' `options(shiny.port = XX)`), then that port will be used. Otherwise,
#' use a random port.
#' @param launch.browser If true, the system's default web browser will be
#' launched automatically after the app is started. Defaults to true in
#' interactive sessions only. This value of this parameter can also be a
#' function to call with the application's URL.
#' @param host The IPv4 address that the application should listen on. Defaults
#' to the `shiny.host` option, if set, or `"127.0.0.1"` if not. See
#' Details.
#' @param workerId Can generally be ignored. Exists to help some editions of
#' Shiny Server Pro route requests to the correct process.
#' @param quiet Should Shiny status messages be shown? Defaults to FALSE.
#' @param display.mode The mode in which to display the application. If set to
#' the value `"showcase"`, shows application code and metadata from a
#' `DESCRIPTION` file in the application directory alongside the
#' application. If set to `"normal"`, displays the application normally.
#' Defaults to `"auto"`, which displays the application in the mode given
#' in its `DESCRIPTION` file, if any.
#' @param test.mode Should the application be launched in test mode? This is
#' only used for recording or running automated tests. Defaults to the
#' `shiny.testmode` option, or FALSE if the option is not set.
#'
#' @examples
#' \dontrun{
#' # Start app in the current working directory
#' runApp()
#'
#' # Start app in a subdirectory called myapp
#' runApp("myapp")
#' }
#'
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#' options(device.ask.default = FALSE)
#'
#' # Apps can be run without a server.r and ui.r file
#' runApp(list(
#' ui = bootstrapPage(
#' numericInput('n', 'Number of obs', 100),
#' plotOutput('plot')
#' ),
#' server = function(input, output) {
#' output$plot <- renderPlot({ hist(runif(input$n)) })
#' }
#' ))
#'
#'
#' # Running a Shiny app object
#' app <- shinyApp(
#' ui = bootstrapPage(
#' numericInput('n', 'Number of obs', 100),
#' plotOutput('plot')
#' ),
#' server = function(input, output) {
#' output$plot <- renderPlot({ hist(runif(input$n)) })
#' }
#' )
#' runApp(app)
#' }
#' @export
runApp <- function(appDir=getwd(),
port=getOption('shiny.port'),
launch.browser=getOption('shiny.launch.browser',
interactive()),
host=getOption('shiny.host', '127.0.0.1'),
workerId="", quiet=FALSE,
display.mode=c("auto", "normal", "showcase"),
test.mode=getOption('shiny.testmode', FALSE)) {
on.exit({
handlerManager$clear()
}, add = TRUE)
if (.globals$running) {
stop("Can't call `runApp()` from within `runApp()`. If your ",
"application code contains `runApp()`, please remove it.")
}
.globals$running <- TRUE
on.exit({
.globals$running <- FALSE
}, add = TRUE)
# Enable per-app Shiny options, for shinyOptions() and getShinyOption().
oldOptionSet <- .globals$options
on.exit({
.globals$options <- oldOptionSet
},add = TRUE)
# A unique identifier associated with this run of this application. It is
# shared across sessions.
shinyOptions(appToken = createUniqueId(8))
# Make warnings print immediately
# Set pool.scheduler to support pool package
ops <- options(
# Raise warn level to 1, but don't lower it
warn = max(1, getOption("warn", default = 1)),
pool.scheduler = scheduleTask
)
on.exit(options(ops), add = TRUE)
# Set up default cache for app.
if (is.null(getShinyOption("cache"))) {
shinyOptions(cache = MemoryCache$new())
}
# Invoke user-defined onStop callbacks, before the application's internal
# onStop callbacks.
on.exit({
.globals$onStopCallbacks$invoke()
.globals$onStopCallbacks <- Callbacks$new()
}, add = TRUE)
require(shiny)
appParts <- as.shiny.appobj(appDir)
# The lines below set some of the app's running options, which
# can be:
# - left unspeficied (in which case the arguments' default
# values from `runApp` kick in);
# - passed through `shinyApp`
# - passed through `runApp` (this function)
# - passed through both `shinyApp` and `runApp` (the latter
# takes precedence)
#
# Matrix of possibilities:
# | IN shinyApp | IN runApp | result | check |
# |-------------|-----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------|
# | no | no | use defaults | exhaust all possibilities: if it's missing (runApp does not specify); THEN if it's not in shinyApp appParts$options; THEN use defaults |
# | yes | no | use shinyApp | if it's missing (runApp does not specify); THEN if it's in shinyApp appParts$options; THEN use shinyApp |
# | no | yes | use runApp | if it's not missing (runApp specifies), use those |
# | yes | yes | use runApp | if it's not missing (runApp specifies), use those |
#
# I tried to make this as compact and intuitive as possible,
# given that there are four distinct possibilities to check
appOps <- appParts$options
findVal <- function(arg, default) {
if (arg %in% names(appOps)) appOps[[arg]] else default
}
if (missing(port))
port <- findVal("port", port)
if (missing(launch.browser))
launch.browser <- findVal("launch.browser", launch.browser)
if (missing(host))
host <- findVal("host", host)
if (missing(quiet))
quiet <- findVal("quiet", quiet)
if (missing(display.mode))
display.mode <- findVal("display.mode", display.mode)
if (missing(test.mode))
test.mode <- findVal("test.mode", test.mode)
if (is.null(host) || is.na(host)) host <- '0.0.0.0'
workerId(workerId)
if (inShinyServer()) {
# If SHINY_PORT is set, we're running under Shiny Server. Check the version
# to make sure it is compatible. Older versions of Shiny Server don't set
# SHINY_SERVER_VERSION, those will return "" which is considered less than
# any valid version.
ver <- Sys.getenv('SHINY_SERVER_VERSION')
if (utils::compareVersion(ver, .shinyServerMinVersion) < 0) {
warning('Shiny Server v', .shinyServerMinVersion,
' or later is required; please upgrade!')
}
}
# Showcase mode is disabled by default; it must be explicitly enabled in
# either the DESCRIPTION file for directory-based apps, or via
# the display.mode parameter. The latter takes precedence.
setShowcaseDefault(0)
.globals$testMode <- test.mode
if (test.mode) {
message("Running application in test mode.")
}
# If appDir specifies a path, and display mode is specified in the
# DESCRIPTION file at that path, apply it here.
if (is.character(appDir)) {
# if appDir specifies a .R file (single-file Shiny app), look for the
# DESCRIPTION in the parent directory
desc <- file.path.ci(
if (tolower(tools::file_ext(appDir)) == "r")
dirname(appDir)
else
appDir, "DESCRIPTION")
if (file.exists(desc)) {
con <- file(desc, encoding = checkEncoding(desc))
on.exit(close(con), add = TRUE)
settings <- read.dcf(con)
if ("DisplayMode" %in% colnames(settings)) {
mode <- settings[1, "DisplayMode"]
if (mode == "Showcase") {
setShowcaseDefault(1)
if ("IncludeWWW" %in% colnames(settings)) {
.globals$IncludeWWW <- as.logical(settings[1, "IncludeWWW"])
if (is.na(.globals$IncludeWWW)) {
stop("In your Description file, `IncludeWWW` ",
"must be set to `True` (default) or `False`")
}
} else {
.globals$IncludeWWW <- TRUE
}
}
}
}
}
## default is to show the .js, .css and .html files in the www directory
## (if not in showcase mode, this variable will simply be ignored)
if (is.null(.globals$IncludeWWW) || is.na(.globals$IncludeWWW)) {
.globals$IncludeWWW <- TRUE
}
# If display mode is specified as an argument, apply it (overriding the
# value specified in DESCRIPTION, if any).
display.mode <- match.arg(display.mode)
if (display.mode == "normal") {
setShowcaseDefault(0)
}
else if (display.mode == "showcase") {
setShowcaseDefault(1)
}
# determine port if we need to
if (is.null(port)) {
# Try up to 20 random ports. If we don't succeed just plow ahead
# with the final value we tried, and let the "real" startServer
# somewhere down the line fail and throw the error to the user.
#
# If we (think we) succeed, save the value as .globals$lastPort,
# and try that first next time the user wants a random port.
for (i in 1:20) {
if (!is.null(.globals$lastPort)) {
port <- .globals$lastPort
.globals$lastPort <- NULL
}
else {
# Try up to 20 random ports
while (TRUE) {
port <- p_randomInt(3000, 8000)
# Reject ports in this range that are considered unsafe by Chrome
# http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
# https://github.com/rstudio/shiny/issues/1784
if (!port %in% c(3659, 4045, 6000, 6665:6669, 6697)) {
break
}
}
}
# Test port to see if we can use it
tmp <- try(startServer(host, port, list()), silent=TRUE)
if (!inherits(tmp, 'try-error')) {
stopServer(tmp)
.globals$lastPort <- port
break
}
}
}
# Extract appOptions (which is a list) and store them as shinyOptions, for
# this app. (This is the only place we have to store settings that are
# accessible both the UI and server portion of the app.)
unconsumeAppOptions(appParts$appOptions)
# Set up the onStop before we call onStart, so that it gets called even if an
# error happens in onStart.
if (!is.null(appParts$onStop))
on.exit(appParts$onStop(), add = TRUE)
if (!is.null(appParts$onStart))
appParts$onStart()
server <- startApp(appParts, port, host, quiet)
# Make the httpuv server object accessible. Needed for calling
# addResourcePath while app is running.
shinyOptions(server = server)
on.exit({
stopServer(server)
}, add = TRUE)
if (!is.character(port)) {
browseHost <- host
if (identical(host, "0.0.0.0")) {
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
browseHost <- "127.0.0.1"
} else if (identical(host, "::")) {
browseHost <- "::1"
}
if (httpuv::ipFamily(browseHost) == 6L) {
browseHost <- paste0("[", browseHost, "]")
}
appUrl <- paste("http://", browseHost, ":", port, sep="")
if (is.function(launch.browser))
launch.browser(appUrl)
else if (launch.browser)
utils::browseURL(appUrl)
} else {
appUrl <- NULL
}
# call application hooks
callAppHook("onAppStart", appUrl)
on.exit({
callAppHook("onAppStop", appUrl)
}, add = TRUE)
.globals$reterror <- NULL
.globals$retval <- NULL
.globals$stopped <- FALSE
# Top-level ..stacktraceoff..; matches with ..stacktraceon in observe(),
# reactive(), Callbacks$invoke(), and others
..stacktraceoff..(
captureStackTraces({
while (!.globals$stopped) {
..stacktracefloor..(serviceApp())
}
})
)
if (isTRUE(.globals$reterror)) {
stop(.globals$retval)
}
else if (.globals$retval$visible)
.globals$retval$value
else
invisible(.globals$retval$value)
}
#' Stop the currently running Shiny app
#'
#' Stops the currently running Shiny app, returning control to the caller of
#' [runApp()].
#'
#' @param returnValue The value that should be returned from
#' [runApp()].
#' @export
stopApp <- function(returnValue = invisible()) {
# reterror will indicate whether retval is an error (i.e. it should be passed
# to stop() when the serviceApp loop stops) or a regular value (in which case
# it should simply be returned with the appropriate visibility).
.globals$reterror <- FALSE
..stacktraceoff..(
tryCatch(
{
captureStackTraces(
.globals$retval <- withVisible(..stacktraceon..(force(returnValue)))
)
},
error = function(e) {
.globals$retval <- e
.globals$reterror <- TRUE
}
)
)
.globals$stopped <- TRUE
httpuv::interrupt()
}
#' Run Shiny Example Applications
#'
#' Launch Shiny example applications, and optionally, your system's web browser.
#'
#' @param example The name of the example to run, or `NA` (the default) to
#' list the available examples.
#' @param launch.browser If true, the system's default web browser will be
#' launched automatically after the app is started. Defaults to true in
#' interactive sessions only.
#' @param host The IPv4 address that the application should listen on. Defaults
#' to the `shiny.host` option, if set, or `"127.0.0.1"` if not.
#' @param display.mode The mode in which to display the example. Defaults to
#' `showcase`, but may be set to `normal` to see the example without
#' code or commentary.
#' @inheritParams runApp
#'
#' @examples
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#' # List all available examples
#' runExample()
#'
#' # Run one of the examples
#' runExample("01_hello")
#'
#' # Print the directory containing the code for all examples
#' system.file("examples", package="shiny")
#' }
#' @export
runExample <- function(example=NA,
port=getOption("shiny.port"),
launch.browser=getOption('shiny.launch.browser',
interactive()),
host=getOption('shiny.host', '127.0.0.1'),
display.mode=c("auto", "normal", "showcase")) {
examplesDir <- system.file('examples', package='shiny')
dir <- resolve(examplesDir, example)
if (is.null(dir)) {
if (is.na(example)) {
errFun <- message
errMsg <- ''
}
else {
errFun <- stop
errMsg <- paste('Example', example, 'does not exist. ')
}
errFun(errMsg,
'Valid examples are "',
paste(list.files(examplesDir), collapse='", "'),
'"')
}
else {
runApp(dir, port = port, host = host, launch.browser = launch.browser,
display.mode = display.mode)
}
}
#' Run a gadget
#'
#' Similar to `runApp`, but handles `input$cancel` automatically, and
#' if running in RStudio, defaults to viewing the app in the Viewer pane.
#'
#' @param app Either a Shiny app object as created by
#' [`shinyApp()`][shiny] et al, or, a UI object.
#' @param server Ignored if `app` is a Shiny app object; otherwise, passed
#' along to `shinyApp` (i.e. `shinyApp(ui = app, server = server)`).
#' @param port See [`runApp()`][shiny].
#' @param viewer Specify where the gadget should be displayed--viewer pane,
#' dialog window, or external browser--by passing in a call to one of the
#' [viewer()] functions.
#' @param stopOnCancel If `TRUE` (the default), then an `observeEvent`
#' is automatically created that handles `input$cancel` by calling
#' `stopApp()` with an error. Pass `FALSE` if you want to handle
#' `input$cancel` yourself.
#' @return The value returned by the gadget.
#'
#' @examples
#' \dontrun{
#' library(shiny)
#'
#' ui <- fillPage(...)
#'
#' server <- function(input, output, session) {
#' ...
#' }
#'
#' # Either pass ui/server as separate arguments...
#' runGadget(ui, server)
#'
#' # ...or as a single app object
#' runGadget(shinyApp(ui, server))
#' }
#' @export
runGadget <- function(app, server = NULL, port = getOption("shiny.port"),
viewer = paneViewer(), stopOnCancel = TRUE) {
if (!is.shiny.appobj(app)) {
app <- shinyApp(app, server)
}
if (isTRUE(stopOnCancel)) {
app <- decorateServerFunc(app, function(input, output, session) {
observeEvent(input$cancel, {
stopApp(stop("User cancel", call. = FALSE))
})
})
}
if (is.null(viewer)) {
viewer <- utils::browseURL
}
shiny::runApp(app, port = port, launch.browser = viewer)
}
# Add custom functionality to a Shiny app object's server func
decorateServerFunc <- function(appobj, serverFunc) {
origServerFuncSource <- appobj$serverFuncSource
appobj$serverFuncSource <- function() {
origServerFunc <- origServerFuncSource()
function(input, output, session) {
serverFunc(input, output, session)
# The clientData and session arguments are optional; check if
# each exists
args <- argsForServerFunc(origServerFunc, session)
do.call(origServerFunc, args)
}
}
appobj
}
#' Viewer options
#'
#' Use these functions to control where the gadget is displayed in RStudio (or
#' other R environments that emulate RStudio's viewer pane/dialog APIs). If
#' viewer APIs are not available in the current R environment, then the gadget
#' will be displayed in the system's default web browser (see
#' [utils::browseURL()]).
#'
#' @return A function that takes a single `url` parameter, suitable for
#' passing as the `viewer` argument of [runGadget()].
#'
#' @rdname viewer
#' @name viewer
NULL
#' @param minHeight The minimum height (in pixels) desired to show the gadget in
#' the viewer pane. If a positive number, resize the pane if necessary to show
#' at least that many pixels. If `NULL`, use the existing viewer pane
#' size. If `"maximize"`, use the maximum available vertical space.
#' @rdname viewer
#' @export
paneViewer <- function(minHeight = NULL) {
viewer <- getOption("viewer")
if (is.null(viewer)) {
utils::browseURL
} else {
function(url) {
viewer(url, minHeight)
}
}
}
#' @param dialogName The window title to display for the dialog.
#' @param width,height The desired dialog width/height, in pixels.
#' @rdname viewer
#' @export
dialogViewer <- function(dialogName, width = 600, height = 600) {
viewer <- getOption("shinygadgets.showdialog")
if (is.null(viewer)) {
utils::browseURL
} else {
function(url) {
viewer(dialogName, url, width = width, height = height)
}
}
}
#' @param browser See [utils::browseURL()].
#' @rdname viewer
#' @export
browserViewer <- function(browser = getOption("browser")) {
function(url) {
utils::browseURL(url, browser = browser)
}
}
# Returns TRUE if we're running in Shiny Server or other hosting environment,
# otherwise returns FALSE.

View File

@@ -8,31 +8,55 @@ getShinyOption <- function(name, default = NULL) {
# Make sure to use named (not numeric) indexing
name <- as.character(name)
if (name %in% names(.globals$options))
.globals$options[[name]]
else
default
# Check if there's a current session
session <- getDefaultReactiveDomain()
if (!is.null(session)) {
if (name %in% names(session$options)) {
return(session$options[[name]])
} else {
return(default)
}
}
# Check if there's a current app
app_state <- getCurrentAppState()
if (!is.null(app_state)) {
if (name %in% names(app_state$options)) {
return(app_state$options[[name]])
} else {
return(default)
}
}
# If we got here, look in global options
if (name %in% names(.globals$options)) {
return(.globals$options[[name]])
} else {
return(default)
}
}
#' Get or set Shiny options
#'
#' `getShinyOption()` retrieves the value of a Shiny option. `shinyOptions()`
#' sets the value of Shiny options; it can also be used to return a list of all
#' currently-set Shiny options.
#' @description
#'
#' @section Scope:
#' There is a global option set which is available by default. When a Shiny
#' application is run with [runApp()], that option set is duplicated and the
#' new option set is available for getting or setting values. If options
#' are set from `global.R`, `app.R`, `ui.R`, or `server.R`, or if they are set
#' from inside the server function, then the options will be scoped to the
#' application. When the application exits, the new option set is discarded and
#' the global option set is restored.
#' There are two mechanisms for working with options for Shiny. One is the
#' [options()] function, which is part of base R, and the other is the
#' `shinyOptions()` function, which is in the Shiny package. The reason for
#' these two mechanisms is has to do with legacy code and scoping.
#'
#' @section Options:
#' There are a number of global options that affect Shiny's behavior. These can
#' be set globally with `options()` or locally (for a single app) with
#' `shinyOptions()`.
#' The [options()] function sets options globally, for the duration of the R
#' process. The [getOption()] function retrieves the value of an option. All
#' shiny related options of this type are prefixed with `"shiny."`.
#'
#' The `shinyOptions()` function sets the value of a shiny option, but unlike
#' `options()`, it is not always global in scope; the options may be scoped
#' globally, to an application, or to a user session in an application,
#' depending on the context. The `getShinyOption()` function retrieves a value
#' of a shiny option. Currently, the options set via `shinyOptions` are for
#' internal use only.
#'
#' @section Options with `options()`:
#'
#' \describe{
#' \item{shiny.autoreload (defaults to `FALSE`)}{If `TRUE` when a Shiny app is launched, the
@@ -41,16 +65,20 @@ getShinyOption <- function(name, default = NULL) {
#' changes are detected, all connected Shiny sessions are reloaded. This
#' allows for fast feedback loops when tweaking Shiny UI.
#'
#' Since monitoring for changes is expensive (we simply poll for last
#' modified times), this feature is intended only for development.
#' Monitoring for changes is no longer expensive, thanks to the \pkg{watcher}
#' package, but this feature is still intended only for development.
#'
#' You can customize the file patterns Shiny will monitor by setting the
#' shiny.autoreload.pattern option. For example, to monitor only ui.R:
#' `options(shiny.autoreload.pattern = glob2rx("ui.R"))`
#' shiny.autoreload.pattern option. For example, to monitor only `ui.R`:
#' `options(shiny.autoreload.pattern = glob2rx("ui.R"))`.
#'
#' The default polling interval is 500 milliseconds. You can change this
#' by setting e.g. `options(shiny.autoreload.interval = 2000)` (every
#' two seconds).}
#' As mentioned above, Shiny no longer polls watched files for changes.
#' Instead, using \pkg{watcher}, Shiny is notified of file changes as they
#' occur. These changes are batched together within a customizable latency
#' period. You can adjust this period by setting
#' `options(shiny.autoreload.interval = 2000)` (in milliseconds). This value
#' converted to seconds and passed to the `latency` argument of
#' [watcher::watcher()]. The default latency is 250ms.}
#' \item{shiny.deprecation.messages (defaults to `TRUE`)}{This controls whether messages for
#' deprecated functions in Shiny will be printed. See
#' [shinyDeprecated()] for more information.}
@@ -64,12 +92,17 @@ getShinyOption <- function(name, default = NULL) {
#' \item{shiny.host (defaults to `"127.0.0.1"`)}{The IP address that Shiny should listen on. See
#' [runApp()] for more information.}
#' \item{shiny.jquery.version (defaults to `3`)}{The major version of jQuery to use.
#' Currently only values of `3` or `1` are supported. If `1`, then jQuery 1.12.4 is used. If `3`,
#' then jQuery 3.5.1 is used.}
#' \item{shiny.json.digits (defaults to `16`)}{The number of digits to use when converting
#' numbers to JSON format to send to the client web browser.}
#' Currently only values of `3` or `1` are supported. If `1`, then jQuery 1.12.4 is used. If `3`,
#' then jQuery `r version_jquery` is used.}
#' \item{shiny.json.digits (defaults to `I(16)`)}{Max number of digits to use when converting
#' numbers to JSON format to send to the client web browser. Use [I()] to specify significant digits.
#' Use `NA` for max precision.}
#' \item{shiny.launch.browser (defaults to `interactive()`)}{A boolean which controls the default behavior
#' when an app is run. See [runApp()] for more information.}
#' \item{shiny.mathjax.url (defaults to `"https://mathjax.rstudio.com/latest/MathJax.js"`)}{
#' The URL that should be used to load MathJax, via [withMathJax()].}
#' \item{shiny.mathjax.config (defaults to `"config=TeX-AMS-MML_HTMLorMML"`)}{The querystring
#' used to load MathJax, via [withMathJax()].}
#' \item{shiny.maxRequestSize (defaults to 5MB)}{This is a number which specifies the maximum
#' web request size, which serves as a size limit for file uploads.}
#' \item{shiny.minified (defaults to `TRUE`)}{By default
@@ -84,7 +117,7 @@ getShinyOption <- function(name, default = NULL) {
#' production.}
#' \item{shiny.sanitize.errors (defaults to `FALSE`)}{If `TRUE`, then normal errors (i.e.
#' errors not wrapped in `safeError`) won't show up in the app; a simple
#' generic error message is printed instead (the error and strack trace printed
#' generic error message is printed instead (the error and stack trace printed
#' to the console remain unchanged). If you want to sanitize errors in general, but you DO want a
#' particular error `e` to get displayed to the user, then set this option
#' to `TRUE` and use `stop(safeError(e))` for errors you want the
@@ -101,52 +134,166 @@ getShinyOption <- function(name, default = NULL) {
#' console.}
#' \item{shiny.testmode (defaults to `FALSE`)}{If `TRUE`, then various features for testing Shiny
#' applications are enabled.}
#' \item{shiny.snapshotsortc (defaults to `FALSE`)}{If `TRUE`, test snapshot keys
#' for \pkg{shinytest} will be sorted consistently using the C locale. Snapshots
#' retrieved by \pkg{shinytest2} will always sort using the C locale.}
#' \item{shiny.trace (defaults to `FALSE`)}{Print messages sent between the R server and the web
#' browser client to the R console. This is useful for debugging. Possible
#' values are `"send"` (only print messages sent to the client),
#' `"recv"` (only print messages received by the server), `TRUE`
#' (print all messages), or `FALSE` (default; don't print any of these
#' messages).}
#' \item{shiny.usecairo (defaults to `TRUE`)}{This is used to disable graphical rendering by the
#' Cairo package, if it is installed. See [plotPNG()] for more
#' information.}
#' \item{shiny.autoload.r (defaults to `TRUE`)}{If `TRUE`, then the R/
#' of a shiny app will automatically be sourced.}
#' \item{shiny.useragg (defaults to `TRUE`)}{Set to `FALSE` to prevent PNG rendering via the
#' ragg package. See [plotPNG()] for more information.}
#' \item{shiny.usecairo (defaults to `TRUE`)}{Set to `FALSE` to prevent PNG rendering via the
#' Cairo package. See [plotPNG()] for more information.}
#' \item{shiny.devmode (defaults to `NULL`)}{Option to enable Shiny Developer Mode. When set,
#' different default `getOption(key)` values will be returned. See [devmode()] for more details.}
### Not documenting as 'shiny.devmode.verbose' is for niche use only
# ' \item{shiny.devmode.verbose (defaults to `TRUE`)}{If `TRUE`, will display messages printed
# ' about which options are being set. See [devmode()] for more details. }
### (end not documenting 'shiny.devmode.verbose')
### start shiny.client_devmode is primarily for niche, internal shinylive usage
# ' \item{shiny.client_devmode (defaults to `FALSE`)}{If `TRUE`, enables client-
# ' side devmode features. Currently the primary feature is the client-side
# ' error console.}
### end shiny.client_devmode
#' \item{shiny.otel.collect (defaults to `Sys.getenv("SHINY_OTEL_COLLECT",
#' "all")`)}{Determines how Shiny will interact with OpenTelemetry.
#'
#' Supported values:
#' * `"none"` - No Shiny OpenTelemetry tracing.
#' * `"session"` - Adds session start/end spans.
#' * `"reactive_update"` - Spans for any synchronous/asynchronous reactive
#' update. (Includes `"session"` features).
#' * `"reactivity"` - Spans for all reactive expressions and logs for setting
#' reactive vals and values. (Includes `"reactive_update"` features). This
#' option must be set when creating any reactive objects that should record
#' OpenTelemetry spans / logs. See [`withOtelCollect()`] and
#' [`localOtelCollect()`] for ways to set this option locally when creating
#' your reactive expressions.
#' * `"all"` - All Shiny OpenTelemetry tracing. Currently equivalent to
#' `"reactivity"`.
#'
#' This option is useful for debugging and profiling while in production. This
#' option will only be useful if the `otelsdk` package is installed and
#' `otel::is_tracing_enabled()` returns `TRUE`. Please have any OpenTelemetry
#' environment variables set before loading any relevant R packages.
#'
#' To set this option locally within a specific part of your Shiny
#' application, see [`withOtelCollect()`] and [`localOtelCollect()`].}
#' \item{shiny.otel.sanitize.errors (defaults to `TRUE`)}{If `TRUE`, fatal and unhandled errors will be sanitized before being sent to the OpenTelemetry backend. The default value of `TRUE` is set to avoid potentially sending sensitive information to the OpenTelemetry backend. If you want the full error message and stack trace to be sent to the OpenTelemetry backend, set this option to `FALSE` or use `safeError(e)`.}
#' }
#'
#'
#' @section Scoping for `shinyOptions()`:
#'
#' There are three levels of scoping for `shinyOptions()`: global,
#' application, and session.
#'
#' The global option set is available by default. Any calls to
#' `shinyOptions()` and `getShinyOption()` outside of an app will access the
#' global option set.
#'
#' When a Shiny application is run with [runApp()], the global option set is
#' duplicated and the new option set is available at the application level. If
#' options are set from `global.R`, `app.R`, `ui.R`, or `server.R` (but
#' outside of the server function), then the application-level options will be
#' modified.
#'
#' Each time a user session is started, the application-level option set is
#' duplicated, for that session. If the options are set from inside the server
#' function, then they will be scoped to the session.
#'
#' @section Options with `shinyOptions()`:
#'
#' There are a number of global options that affect Shiny's behavior. These
#' can be set globally with `options()` or locally (for a single app) with
#' `shinyOptions()`.
#'
#' \describe{ \item{cache}{A caching object that will be used by
#' [renderCachedPlot()]. If not specified, a [cachem::cache_mem()] will be
#' used.} }
#'
#' @param ... Options to set, with the form `name = value`.
#' @aliases shiny-options
#' @examples
#' \dontrun{
#' shinyOptions(myOption = 10)
#' getShinyOption("myOption")
#' }
#' @export
shinyOptions <- function(...) {
newOpts <- list(...)
newOpts <- list2(...)
if (length(newOpts) > 0) {
# If we're within a session, modify at the session level.
session <- getDefaultReactiveDomain()
if (!is.null(session)) {
# Modify session-level-options
session$options <- dropNulls(mergeVectors(session$options, newOpts))
return(invisible(session$options))
}
# If not in a session, but we have a currently running app, modify options
# at the app level.
app_state <- getCurrentAppState()
if (!is.null(app_state)) {
# Modify app-level options
app_state$options <- dropNulls(mergeVectors(app_state$options, newOpts))
return(invisible(app_state$options))
}
# If no currently running app, modify global options and return them.
.globals$options <- dropNulls(mergeVectors(.globals$options, newOpts))
invisible(.globals$options)
} else {
.globals$options
return(invisible(.globals$options))
}
}
# If not setting any options, just return current option set, visibly.
# Eval an expression with a new option set
withLocalOptions <- function(expr) {
oldOptionSet <- .globals$options
on.exit(.globals$options <- oldOptionSet)
session <- getDefaultReactiveDomain()
if (!is.null(session)) {
return(session$options)
}
expr
app_state <- getCurrentAppState()
if (!is.null(app_state)) {
return(app_state$options)
}
return(.globals$options)
}
# Get specific shiny options and put them in a list, reset those shiny options,
# and then return the options list. This should be during the creation of a
# shiny app object, which happens before another option frame is added to the
# options stack (the new option frame is added when the app is run). This
# function "consumes" the options when the shinyApp object is created, so the
# options won't affect another app that is created later.
consumeAppOptions <- function() {
# shiny app object. This function "consumes" the options when the shinyApp
# object is created, so the options won't affect another app that is created
# later.
#
# ==== Example ====
# shinyOptions(bookmarkStore = 1234)
# # This now returns 1234.
# getShinyOption("bookmarkStore")
#
# # Creating the app captures the bookmarkStore option and clears it.
# s <- shinyApp(
# fluidPage(verbatimTextOutput("txt")),
# function(input, output) {
# output$txt <- renderText(getShinyOption("bookmarkStore"))
# }
# )
#
# # This now returns NULL.
# getShinyOption("bookmarkStore")
#
# When running the app, the app will display "1234"
# runApp(s)
#
# # After quitting the app, this still returns NULL.
# getShinyOption("bookmarkStore")
# ==================
#
# If another app had been created after s was created, but before s was run,
# then it would capture the value of "bookmarkStore" at the time of creation.
captureAppOptions <- function() {
options <- list(
appDir = getwd(),
bookmarkStore = getShinyOption("bookmarkStore")
@@ -157,9 +304,9 @@ consumeAppOptions <- function() {
options
}
# Do the inverse of consumeAppOptions. This should be called once the app is
# Do the inverse of captureAppOptions. This should be called once the app is
# started.
unconsumeAppOptions <- function(options) {
applyCapturedAppOptions <- function(options) {
if (!is.null(options)) {
do.call(shinyOptions, options)
}

44
R/shiny-package.R Normal file
View File

@@ -0,0 +1,44 @@
# See also R/reexports.R
## usethis namespace: start
#' @importFrom lifecycle deprecated is_present
#' @importFrom grDevices dev.set dev.cur
#' @importFrom fastmap fastmap
#' @importFrom promises
#' %...!% %...>%
#' as.promise is.promising is.promise
#' promise_resolve promise_reject
#' hybrid_then
#' with_promise_domain new_promise_domain
#' @importFrom rlang
#' quo enquo enquo0 as_function get_expr get_env new_function enquos
#' eval_tidy expr pairlist2 new_quosure enexpr as_quosure is_quosure inject
#' quo_set_env quo_set_expr quo_get_expr
#' enquos0 zap_srcref %||% is_na
#' is_false list2
#' missing_arg is_missing maybe_missing
#' quo_is_missing fn_fmls<- fn_body fn_body<-
#' check_dots_empty check_dots_unnamed
#' @import htmltools
#' @import httpuv
#' @import xtable
#' @import R6
#' @import mime
## usethis namespace: end
NULL
# It's necessary to Depend on methods so Rscript doesn't fail. It's necessary
# to import(methods) in NAMESPACE so R CMD check doesn't complain. This
# approach isn't foolproof because Rscript -e pkgname::func() doesn't actually
# cause methods to be attached, but it's not a problem for shiny::runApp()
# since we call require(shiny) as part of loading the app.
#' @import methods
NULL
# For usethis::use_release_issue()
release_bullets <- function() {
c(
"Update static imports: `staticimports::import()`"
)
}

717
R/shiny.R

File diff suppressed because it is too large Load Diff

View File

@@ -93,8 +93,7 @@ shinyApp <- function(ui, server, onStart=NULL, options=list(),
# Store the appDir and bookmarking-related options, so that we can read them
# from within the app.
shinyOptions(appDir = getwd())
appOptions <- consumeAppOptions()
appOptions <- captureAppOptions()
structure(
list(
@@ -114,7 +113,10 @@ shinyApp <- function(ui, server, onStart=NULL, options=list(),
#' @export
shinyAppDir <- function(appDir, options=list()) {
if (!utils::file_test('-d', appDir)) {
stop("No Shiny application exists at the path \"", appDir, "\"")
rlang::abort(
paste0("No Shiny application exists at the path \"", appDir, "\""),
class = "invalidShinyAppDir"
)
}
# In case it's a relative path, convert to absolute (so we're not adversely
@@ -126,7 +128,10 @@ shinyAppDir <- function(appDir, options=list()) {
} else if (file.exists.ci(appDir, "app.R")) {
shinyAppDir_appR("app.R", appDir, options = options)
} else {
stop("App dir must contain either app.R or server.R.")
rlang::abort(
"App dir must contain either app.R or server.R.",
class = "invalidShinyAppDir"
)
}
}
@@ -157,11 +162,29 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
sharedEnv <- globalenv()
}
# To enable hot-reloading of support files, this function is called
# whenever the UI or Server func source is updated. To avoid loading
# support files 2x, we follow the last cache update trigger timestamp.
autoload_r_support_if_needed <- local({
autoload_last_loaded <- -1
function() {
if (!isTRUE(getOption("shiny.autoload.r", TRUE))) return()
last_cache_trigger <- cachedAutoReloadLastChanged$get()
if (identical(autoload_last_loaded, last_cache_trigger)) return()
loadSupport(appDir, renv = sharedEnv, globalrenv = globalenv())
autoload_last_loaded <<- last_cache_trigger
}
})
# uiHandlerSource is a function that returns an HTTP handler for serving up
# ui.R as a webpage. The "cachedFuncWithFile" call makes sure that the closure
# we're creating here only gets executed when ui.R's contents change.
uiHandlerSource <- cachedFuncWithFile(appDir, "ui.R", case.sensitive = FALSE,
function(uiR) {
autoload_r_support_if_needed()
if (file.exists(uiR)) {
# If ui.R contains a call to shinyUI (which sets .globals$ui), use that.
# If not, then take the last expression that's returned from ui.R.
@@ -188,10 +211,11 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
staticPaths <- list()
}
fallbackWWWDir <- system.file("www-dir", package = "shiny")
fallbackWWWDir <- system_file("www-dir", package = "shiny")
serverSource <- cachedFuncWithFile(appDir, "server.R", case.sensitive = FALSE,
function(serverR) {
autoload_r_support_if_needed()
# If server.R contains a call to shinyServer (which sets .globals$server),
# use that. If not, then take the last expression that's returned from
# server.R.
@@ -227,10 +251,9 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
onStart <- function() {
oldwd <<- getwd()
setwd(appDir)
# TODO: we should support hot reloading on global.R and R/*.R changes.
if (getOption("shiny.autoload.r", TRUE)) {
loadSupport(appDir, renv=sharedEnv, globalrenv=globalenv())
} else {
autoload_r_support_if_needed()
} else {
if (file.exists(file.path.ci(appDir, "global.R")))
sourceUTF8(file.path.ci(appDir, "global.R"))
}
@@ -281,37 +304,81 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
#
# The return value is a function that halts monitoring when called.
initAutoReloadMonitor <- function(dir) {
if (!getOption("shiny.autoreload", FALSE)) {
if (!get_devmode_option("shiny.autoreload", FALSE)) {
return(function(){})
}
filePattern <- getOption("shiny.autoreload.pattern",
".*\\.(r|html?|js|css|png|jpe?g|gif)$")
filePattern <- getOption(
"shiny.autoreload.pattern",
".*\\.(r|html?|js|css|png|jpe?g|gif)$"
)
lastValue <- NULL
observeLabel <- paste0("File Auto-Reload - '", basename(dir), "'")
obs <- observe(label = observeLabel, {
files <- sort_c(
list.files(dir, pattern = filePattern, recursive = TRUE, ignore.case = TRUE)
)
times <- file.info(files)$mtime
names(times) <- files
if (is.null(lastValue)) {
# First run
lastValue <<- times
} else if (!identical(lastValue, times)) {
# We've changed!
lastValue <<- times
if (is_installed("watcher")) {
check_for_update <- function(paths) {
paths <- grep(
filePattern,
paths,
ignore.case = TRUE,
value = TRUE
)
if (length(paths) == 0) {
return()
}
cachedAutoReloadLastChanged$set()
autoReloadCallbacks$invoke()
}
# [garrick, 2025-02-20] Shiny <= v1.10.0 used `invalidateLater()` with an
# autoreload.interval in ms. {watcher} instead uses a latency parameter in
# seconds, which serves a similar purpose and that I'm keeping for backcompat.
latency <- getOption("shiny.autoreload.interval", 250) / 1000
watcher <- watcher::watcher(dir, check_for_update, latency = latency)
watcher$start()
onStop(watcher$stop)
} else {
# Fall back to legacy observer behavior
if (!is_false(getOption("shiny.autoreload.legacy_warning", TRUE))) {
cli::cli_warn(
c(
"Using legacy autoreload file watching. Please install {.pkg watcher} for a more performant autoreload file watcher.",
"i" = "Set {.run options(shiny.autoreload.legacy_warning = FALSE)} to suppress this warning."
),
.frequency = "regularly",
.frequency_id = "shiny.autoreload.legacy_warning"
)
}
invalidateLater(getOption("shiny.autoreload.interval", 500))
})
lastValue <- NULL
observeLabel <- paste0("File Auto-Reload - '", basename(dir), "'")
watcher <- observe(label = observeLabel, {
files <- sort_c(
list.files(dir, pattern = filePattern, recursive = TRUE, ignore.case = TRUE)
)
times <- file.info(files)$mtime
names(times) <- files
if (is.null(lastValue)) {
# First run
lastValue <<- times
} else if (!identical(lastValue, times)) {
# We've changed!
lastValue <<- times
cachedAutoReloadLastChanged$set()
autoReloadCallbacks$invoke()
}
invalidateLater(getOption("shiny.autoreload.interval", 500))
})
onStop(watcher$destroy)
watcher$destroy
}
onStop(obs$destroy)
obs$destroy
invisible(watcher)
}
#' Load an app's supporting R files
@@ -334,7 +401,7 @@ initAutoReloadMonitor <- function(dir) {
#' @param appDir The application directory. If `appDir` is `NULL` or
#' not supplied, the nearest enclosing directory that is a Shiny app, starting
#' with the current directory, is used.
#' @param renv The environmeny in which the files in the `R/` directory should
#' @param renv The environment in which the files in the `R/` directory should
#' be evaluated.
#' @param globalrenv The environment in which `global.R` should be evaluated. If
#' `NULL`, `global.R` will not be evaluated at all.
@@ -360,10 +427,12 @@ loadSupport <- function(appDir=NULL, renv=new.env(parent=globalenv()), globalren
helpersDir <- file.path(appDir, "R")
disabled <- list.files(helpersDir, pattern="^_disable_autoload\\.r$", recursive=FALSE, ignore.case=TRUE)
if (length(disabled) > 0){
if (length(disabled) > 0) {
return(invisible(renv))
}
warn_if_app_dir_is_package(appDir)
helpers <- list.files(helpersDir, pattern="\\.[rR]$", recursive=FALSE, full.names=TRUE)
# Ensure files in R/ are sorted according to the 'C' locale before sourcing.
# This convention is based on the default for packages. For details, see:
@@ -378,6 +447,27 @@ loadSupport <- function(appDir=NULL, renv=new.env(parent=globalenv()), globalren
invisible(renv)
}
warn_if_app_dir_is_package <- function(appDir) {
has_namespace <- file.exists(file.path.ci(appDir, "NAMESPACE"))
has_desc_pkg <- FALSE
if (!has_namespace) {
descFile <- file.path.ci(appDir, "DESCRIPTION")
has_desc_pkg <-
file.exists(descFile) &&
identical(as.character(read.dcf(descFile, fields = "Type")), "Package")
}
if (has_namespace || has_desc_pkg) {
warning(
"Loading R/ subdirectory for Shiny application, but this directory appears ",
"to contain an R package. Sourcing files in R/ may cause unexpected behavior. ",
"See `?loadSupport` for more details."
)
}
}
# This reads in an app dir for a single-file application (e.g. app.R), and
# returns a shiny.appobj.
# appDir must be a normalized (absolute) path, not a relative one
@@ -393,8 +483,6 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
wasDir <- setwd(appDir)
on.exit(setwd(wasDir))
# TODO: we should support hot reloading on R/*.R changes.
# In an upcoming version of shiny, this option will go away.
if (getOption("shiny.autoload.r", TRUE)) {
# Create a child env which contains all the helpers and will be the shared parent
# of the ui.R and server.R load.
@@ -408,7 +496,7 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
if (!is.shiny.appobj(result))
stop("app.R did not return a shiny.appobj object.")
unconsumeAppOptions(result$appOptions)
applyCapturedAppOptions(result$appOptions)
return(result)
}
@@ -439,7 +527,7 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
staticPaths <- list()
}
fallbackWWWDir <- system.file("www-dir", package = "shiny")
fallbackWWWDir <- system_file("www-dir", package = "shiny")
oldwd <- NULL
monitorHandle <- NULL
@@ -559,7 +647,7 @@ as.tags.shiny.appobj <- function(x, ...) {
# jcheng 06/06/2014: Unfortunate copy/paste between this function and
# knit_print.shiny.appobj, but I am trying to make the most conservative
# change possible due to upcoming release.
opts <- x$options %OR% list()
opts <- x$options %||% list()
width <- if (is.null(opts$width)) "100%" else opts$width
height <- if (is.null(opts$height)) "400" else opts$height
@@ -579,84 +667,3 @@ deferredIFrame <- function(path, width, height) {
class = "shiny-frame shiny-frame-deferred"
)
}
#' Knitr S3 methods
#'
#' These S3 methods are necessary to help Shiny applications and UI chunks embed
#' themselves in knitr/rmarkdown documents.
#'
#' @name knitr_methods
#' @param x Object to knit_print
#' @param ... Additional knit_print arguments
NULL
# If there's an R Markdown runtime option set but it isn't set to Shiny, then
# return a warning indicating the runtime is inappropriate for this object.
# Returns NULL in all other cases.
shiny_rmd_warning <- function() {
runtime <- knitr::opts_knit$get("rmarkdown.runtime")
if (!is.null(runtime) && runtime != "shiny")
# note that the RStudio IDE checks for this specific string to detect Shiny
# applications in static document
list(structure(
"Shiny application in a static R Markdown document",
class = "rmd_warning"))
else
NULL
}
#' @rdname knitr_methods
knit_print.shiny.appobj <- function(x, ...) {
opts <- x$options %OR% list()
width <- if (is.null(opts$width)) "100%" else opts$width
height <- if (is.null(opts$height)) "400" else opts$height
runtime <- knitr::opts_knit$get("rmarkdown.runtime")
if (!is.null(runtime) && runtime != "shiny") {
# If not rendering to a Shiny document, create a box exactly the same
# dimensions as the Shiny app would have had (so the document continues to
# flow as it would have with the app), and display a diagnostic message
width <- validateCssUnit(width)
height <- validateCssUnit(height)
output <- tags$div(
style=paste("width:", width, "; height:", height, "; text-align: center;",
"box-sizing: border-box;", "-moz-box-sizing: border-box;",
"-webkit-box-sizing: border-box;"),
class="muted well",
"Shiny applications not supported in static R Markdown documents")
}
else {
path <- addSubApp(x)
output <- deferredIFrame(path, width, height)
}
# If embedded Shiny apps ever have JS/CSS dependencies (like pym.js) we'll
# need to grab those and put them in meta, like in knit_print.shiny.tag. But
# for now it's not an issue, so just return the HTML and warning.
knitr::asis_output(htmlPreserve(format(output, indent=FALSE)),
meta = shiny_rmd_warning(), cacheable = FALSE)
}
# Let us use a nicer syntax in knitr chunks than literally
# calling output$value <- renderFoo(...) and fooOutput().
#' @rdname knitr_methods
#' @param inline Whether the object is printed inline.
knit_print.shiny.render.function <- function(x, ..., inline = FALSE) {
x <- htmltools::as.tags(x, inline = inline)
output <- knitr::knit_print(tagList(x))
attr(output, "knit_cacheable") <- FALSE
attr(output, "knit_meta") <- append(attr(output, "knit_meta"),
shiny_rmd_warning())
output
}
# Lets us drop reactive expressions directly into a knitr chunk and have the
# value printed out! Nice for teaching if nothing else.
#' @rdname knitr_methods
knit_print.reactive <- function(x, ..., inline = FALSE) {
renderFunc <- if (inline) renderText else renderPrint
knitr::knit_print(renderFunc({
x()
}), inline = inline)
}

View File

@@ -14,7 +14,11 @@ NULL
#' # now we can just write "static" content without withMathJax()
#' div("more math here $$\\sqrt{2}$$")
withMathJax <- function(...) {
path <- 'https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'
path <- paste0(
getOption("shiny.mathjax.url", "https://mathjax.rstudio.com/latest/MathJax.js"),
"?",
getOption("shiny.mathjax.config", "config=TeX-AMS-MML_HTMLorMML")
)
tagList(
tags$head(
singleton(tags$script(src = path, type = 'text/javascript'))
@@ -25,6 +29,8 @@ withMathJax <- function(...) {
}
renderPage <- function(ui, showcase=0, testMode=FALSE) {
lang <- getLang(ui)
# If the ui is a NOT complete document (created by htmlTemplate()), then do some
# preprocessing and make sure it's a complete document.
if (!inherits(ui, "html_document")) {
@@ -37,62 +43,157 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
# Put the body into the default template
ui <- htmlTemplate(
system.file("template", "default.html", package = "shiny"),
body = ui
system_file("template", "default.html", package = "shiny"),
lang = lang,
body = ui,
# this template is a complete HTML document
document_ = TRUE
)
}
jquery <- function() {
version <- getOption("shiny.jquery.version", 3)
if (version == 3) {
return(htmlDependency(
"jquery", "3.5.1",
c(href = "shared"),
script = "jquery.min.js"
))
}
if (version == 1) {
return(htmlDependency(
"jquery", "1.12.4",
c(href = "shared/legacy"),
script = "jquery.min.js"
))
}
stop("Unsupported version of jQuery: ", version)
}
shiny_deps <- list(
htmlDependency("json2", "2014.02.04", c(href="shared"), script = "json2-min.js"),
jquery(),
htmlDependency("shiny", utils::packageVersion("shiny"), c(href="shared"),
script = if (getOption("shiny.minified", TRUE)) "shiny.min.js" else "shiny.js",
stylesheet = "shiny.css")
shiny_deps <- c(
list(jqueryDependency()),
shinyDependencies()
)
if (testMode) {
# Add code injection listener if in test mode
shiny_deps[[length(shiny_deps) + 1]] <-
htmlDependency("shiny-testmode", utils::packageVersion("shiny"),
c(href="shared"), script = "shiny-testmode.js")
htmlDependency(
"shiny-testmode",
get_package_version("shiny"),
src = "www/shared",
package = "shiny",
script = "shiny-testmode.js",
all_files = FALSE
)
}
if (in_devmode() || in_client_devmode()) {
# If we're in dev mode, add a simple script to the head that injects a
# global variable for the client to use to detect dev mode.
shiny_deps[[length(shiny_deps) + 1]] <-
htmlDependency(
"shiny-devmode",
get_package_version("shiny"),
src = "www/shared",
package = "shiny",
head="<script>window.__SHINY_DEV_MODE__ = true;</script>",
all_files = FALSE
)
}
html <- renderDocument(ui, shiny_deps, processDep = createWebDependency)
enc2utf8(paste(collapse = "\n", html))
}
jqueryDependency <- function() {
version <- getOption("shiny.jquery.version", 3)
if (version == 3) {
return(htmlDependency(
"jquery", version_jquery,
src = "www/shared",
package = "shiny",
script = "jquery.min.js",
all_files = FALSE
))
}
if (version == 1) {
return(htmlDependency(
"jquery", "1.12.4",
src = "www/shared/legacy",
package = "shiny",
script = "jquery.min.js",
all_files = FALSE
))
}
stop("Unsupported version of jQuery: ", version)
}
shinyDependencies <- function() {
list(
bslib::bs_dependency_defer(shinyDependencyCSS),
busyIndicatorDependency(),
htmlDependency(
name = "shiny-javascript",
version = get_package_version("shiny"),
src = "www/shared",
package = "shiny",
script =
if (isTRUE(
get_devmode_option(
"shiny.minified",
TRUE
)
))
"shiny.min.js"
else
"shiny.js",
all_files = FALSE
)
)
}
shinyDependencySass <- function(bs_version) {
bootstrap_scss <- paste0("shiny.bootstrap", bs_version, ".scss")
scss_home <- system_file("www/shared/shiny_scss", package = "shiny")
scss_files <- file.path(scss_home, c(bootstrap_scss, "shiny.scss"))
lapply(scss_files, sass::sass_file)
}
shinyDependencyCSS <- function(theme) {
version <- get_package_version("shiny")
if (!is_bs_theme(theme)) {
return(htmlDependency(
name = "shiny-css",
version = version,
src = "www/shared",
package = "shiny",
stylesheet = "shiny.min.css",
all_files = FALSE
))
}
bs_version <- bslib::theme_version(theme)
bslib::bs_dependency(
input = shinyDependencySass(bs_version),
theme = theme,
name = "shiny-sass",
version = version,
cache_key_extra = version
)
}
#' Create a Shiny UI handler
#'
#' Historically this function was used in ui.R files to register a user
#' @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
#' ensure that the last expression to be returned from ui.R is a user interface.
#' This function is kept for backwards compatibility with older applications. It
#' returns the value that is passed to it.
#'
#' @param ui A user interace definition
#' @param ui A user interface definition
#' @return The user interface definition, without modifications or side effects.
#' @keywords internal
#' @export
shinyUI <- function(ui) {
if (in_devmode()) {
shinyDeprecated(
"0.10.0", "shinyUI()",
details = paste0(
"When removing `shinyUI()`, ",
"ensure that the last expression returned from ui.R is a user interface ",
"normally supplied to `shinyUI(ui)`."
)
)
}
.globals$ui <- list(ui)
ui
}
@@ -103,7 +204,7 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
allowed_methods <- "GET"
if (is.function(ui)) {
allowed_methods <- attr(ui, "http_methods_supported", exact = TRUE) %OR% allowed_methods
allowed_methods <- attr(ui, "http_methods_supported", exact = TRUE) %||% allowed_methods
}
function(req) {
@@ -120,7 +221,7 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
showcaseMode <- mode
}
testMode <- .globals$testMode %OR% FALSE
testMode <- getShinyOption("testmode", default = FALSE)
# Create a restore context using query string
bookmarkStore <- getShinyOption("bookmarkStore", default = "disable")

View File

@@ -1,34 +1,123 @@
utils::globalVariables('func')
utils::globalVariables('func', add = TRUE)
#' Mark a function as a render function
#'
#' Should be called by implementers of `renderXXX` functions in order to
#' mark their return values as Shiny render functions, and to provide a hint to
#' Shiny regarding what UI function is most commonly used with this type of
#' render function. This can be used in R Markdown documents to create complete
#' output widgets out of just the render function.
#' `r lifecycle::badge("superseded")` Please use [`createRenderFunction()`] to
#' support async execution. (Shiny 1.1.0)
#'
#' Should be called by implementers of `renderXXX` functions in order to mark
#' their return values as Shiny render functions, and to provide a hint to Shiny
#' regarding what UI function is most commonly used with this type of render
#' function. This can be used in R Markdown documents to create complete output
#' widgets out of just the render function.
#'
#' Note that it is generally preferable to use [createRenderFunction()] instead
#' of `markRenderFunction()`. It essentially wraps up the user-provided
#' expression in the `transform` function passed to it, then passes the resulting
#' function to `markRenderFunction()`. It also provides a simpler calling
#' interface. There may be cases where `markRenderFunction()` must be used instead of
#' [createRenderFunction()] -- for example, when the `transform` parameter of
#' [createRenderFunction()] is not flexible enough for your needs.
#'
#' @param uiFunc A function that renders Shiny UI. Must take a single argument:
#' an output ID.
#' @param renderFunc A function that is suitable for assigning to a Shiny output
#' slot.
#' @param outputArgs A list of arguments to pass to the `uiFunc`. Render
#' functions should include `outputArgs = list()` in their own parameter
#' list, and pass through the value to `markRenderFunction`, to allow
#' app authors to customize outputs. (Currently, this is only supported for
#' dynamically generated UIs, such as those created by Shiny code snippets
#' embedded in R Markdown documents).
#' functions should include `outputArgs = list()` in their own parameter list,
#' and pass through the value to `markRenderFunction`, to allow app authors to
#' customize outputs. (Currently, this is only supported for dynamically
#' generated UIs, such as those created by Shiny code snippets embedded in R
#' Markdown documents).
#' @param cacheHint One of `"auto"`, `FALSE`, or some other information to
#' identify this instance for caching using [bindCache()]. If `"auto"`, it
#' will try to automatically infer caching information. If `FALSE`, do not
#' allow caching for the object. Some render functions (such as [renderPlot])
#' contain internal state that makes them unsuitable for caching.
#' @param cacheWriteHook Used if the render function is passed to `bindCache()`.
#' This is an optional callback function to invoke before saving the value
#' from the render function to the cache. This function must accept one
#' argument, the value returned from `renderFunc`, and should return the value
#' to store in the cache.
#' @param cacheReadHook Used if the render function is passed to `bindCache()`.
#' This is an optional callback function to invoke after reading a value from
#' the cache (if there is a cache hit). The function will be passed one
#' argument, the value retrieved from the cache. This can be useful when some
#' side effect needs to occur for a render function to behave correctly. For
#' example, some render functions call [createWebDependency()] so that Shiny
#' is able to serve JS and CSS resources.
#' @return The `renderFunc` function, with annotations.
#'
#' @seealso [createRenderFunction()]
#' @export
markRenderFunction <- function(uiFunc, renderFunc, outputArgs = list()) {
markRenderFunction <- function(
uiFunc,
renderFunc,
outputArgs = list(),
cacheHint = "auto",
cacheWriteHook = NULL,
cacheReadHook = NULL
) {
# (Do not emit warning for superseded code, "since theres no risk if you keep using it")
# # This method is called by the superseding function, createRenderFunction().
# if (in_devmode()) {
# shinyDeprecated("1.1.0", "markRenderFunction()", "createRenderFunction()")
# }
force(renderFunc)
# a mutable object that keeps track of whether `useRenderFunction` has been
# executed (this usually only happens when rendering Shiny code snippets in
# an interactive R Markdown document); its initial value is FALSE
hasExecuted <- Mutable$new()
hasExecuted$set(FALSE)
origRenderFunc <- renderFunc
renderFunc <- function(...) {
if (is.null(uiFunc)) {
uiFunc <- function(id) {
pre(
"No UI/output function provided for render function. ",
"Please see ?shiny::markRenderFunction and ?shiny::createRenderFunction."
)
}
}
if (identical(cacheHint, "auto")) {
origUserFunc <- attr(renderFunc, "wrappedFunc", exact = TRUE)
# The result could be NULL, but don't warn now because it'll only affect
# users if they try to use caching. We'll warn when someone calls
# bindCache() on this object.
if (is.null(origUserFunc)) {
cacheHint <- NULL
} else {
# Add in the wrapper render function and they output function, because
# they can be useful for distinguishing two renderX functions that receive
# the same user expression but do different things with them (like
# renderText and renderPrint).
cacheHint <- list(
origUserFunc = origUserFunc,
renderFunc = renderFunc,
outputFunc = uiFunc
)
}
}
if (!is.null(cacheHint) && !is_false(cacheHint)) {
if (!is.list(cacheHint)) {
cacheHint <- list(cacheHint)
}
# For functions, remove the env and source refs because they can cause
# spurious differences.
# For expressions, remove source refs.
# For everything else, do nothing.
cacheHint <- lapply(cacheHint, function(x) {
if (is.function(x)) formalsAndBody(x)
else if (is_quosure(x)) zap_srcref(quo_get_expr(x))
else if (is.language(x)) zap_srcref(x)
else x
})
}
wrappedRenderFunc <- function(...) {
# if the user provided something through `outputArgs` BUT the
# `useRenderFunction` was not executed, then outputArgs will be ignored,
# so throw a warning to let user know the correct usage
@@ -41,15 +130,34 @@ markRenderFunction <- function(uiFunc, renderFunc, outputArgs = list()) {
# stop warning from happening again for the same object
hasExecuted$set(TRUE)
}
if (is.null(formals(origRenderFunc))) origRenderFunc()
else origRenderFunc(...)
if (is.null(formals(renderFunc))) renderFunc()
else renderFunc(...)
}
structure(renderFunc,
class = c("shiny.render.function", "function"),
outputFunc = uiFunc,
outputArgs = outputArgs,
hasExecuted = hasExecuted)
otelAttrs <-
otel_srcref_attributes(
attr(renderFunc, "wrappedFunc", exact = TRUE),
# Can't retrieve the render function used at this point, so just use NULL
fn_name = NULL
)
ret <- structure(
wrappedRenderFunc,
class = c("shiny.render.function", "function"),
outputFunc = uiFunc,
outputArgs = outputArgs,
hasExecuted = hasExecuted,
cacheHint = cacheHint,
cacheWriteHook = cacheWriteHook,
cacheReadHook = cacheReadHook,
otelAttrs = otelAttrs
)
if (has_otel_collect("reactivity")) {
ret <- enable_otel_shiny_render_function(ret)
}
ret
}
#' @export
@@ -57,7 +165,27 @@ print.shiny.render.function <- function(x, ...) {
cat_line("<shiny.render.function>")
}
#' Implement render functions
#' Implement custom render functions
#'
#' Developer-facing utilities for implementing a custom `renderXXX()` function.
#' Before using these utilities directly, consider using the [`htmlwidgets`
#' package](http://www.htmlwidgets.org/develop_intro.html) to implement custom
#' outputs (i.e., custom `renderXXX()`/`xxxOutput()` functions). That said,
#' these utilities can be used more directly if a full-blown htmlwidget isn't
#' needed and/or the user-supplied reactive expression needs to be wrapped in
#' additional call(s).
#'
#' To implement a custom `renderXXX()` function, essentially 2 things are needed:
#' 1. Capture the user's reactive expression as a function.
#' * New `renderXXX()` functions can use `quoToFunction()` for this, but
#' already existing `renderXXX()` functions that contain `env` and `quoted`
#' parameters may want to continue using `installExprFunction()` for better
#' legacy support (see examples).
#' 2. Flag the resulting function (from 1) as a Shiny rendering function and
#' also provide a UI container for displaying the result of the rendering
#' function.
#' * `createRenderFunction()` is currently recommended (instead of
#' [markRenderFunction()]) for this step (see examples).
#'
#' @param func A function without parameters, that returns user data. If the
#' returned value is a promise, then the render function will proceed in async
@@ -70,34 +198,97 @@ print.shiny.render.function <- function(x, ...) {
#' @param outputFunc The UI function that is used (or most commonly used) with
#' this render function. This can be used in R Markdown documents to create
#' complete output widgets out of just the render function.
#' @param outputArgs A list of arguments to pass to the `outputFunc`.
#' Render functions should include `outputArgs = list()` in their own
#' parameter list, and pass through the value as this argument, to allow app
#' authors to customize outputs. (Currently, this is only supported for
#' dynamically generated UIs, such as those created by Shiny code snippets
#' embedded in R Markdown documents).
#' @inheritParams markRenderFunction
#' @return An annotated render function, ready to be assigned to an
#' `output` slot.
#'
#' @examples
#' # A custom render function that repeats the supplied value 3 times
#' renderTriple <- function(expr) {
#' # Wrap user-supplied reactive expression into a function
#' func <- quoToFunction(rlang::enquo0(expr))
#'
#' createRenderFunction(
#' func,
#' transform = function(value, session, name, ...) {
#' paste(rep(value, 3), collapse=", ")
#' },
#' outputFunc = textOutput
#' )
#' }
#'
#' # For better legacy support, consider using installExprFunction() over quoToFunction()
#' renderTripleLegacy <- function(expr, env = parent.frame(), quoted = FALSE) {
#' func <- installExprFunction(expr, "func", env, quoted)
#'
#' createRenderFunction(
#' func,
#' transform = function(value, session, name, ...) {
#' paste(rep(value, 3), collapse=", ")
#' },
#' outputFunc = textOutput
#' )
#' }
#'
#' # Test render function from the console
#' reactiveConsole(TRUE)
#'
#' v <- reactiveVal("basic")
#' r <- renderTriple({ v() })
#' r()
#' #> [1] "basic, basic, basic"
#'
#' # User can supply quoted code via rlang::quo(). Note that evaluation of the
#' # expression happens when r2() is invoked, not when r2 is created.
#' q <- rlang::quo({ v() })
#' r2 <- rlang::inject(renderTriple(!!q))
#' v("rlang")
#' r2()
#' #> [1] "rlang, rlang, rlang"
#'
#' # Supplying quoted code without rlang::quo() requires installExprFunction()
#' expr <- quote({ v() })
#' r3 <- renderTripleLegacy(expr, quoted = TRUE)
#' v("legacy")
#' r3()
#' #> [1] "legacy, legacy, legacy"
#'
#' # The legacy approach also supports with quosures (env is ignored in this case)
#' q <- rlang::quo({ v() })
#' r4 <- renderTripleLegacy(q, quoted = TRUE)
#' v("legacy-rlang")
#' r4()
#' #> [1] "legacy-rlang, legacy-rlang, legacy-rlang"
#'
#' # Turn off reactivity in the console
#' reactiveConsole(FALSE)
#'
#' @export
createRenderFunction <- function(
func, transform = function(value, session, name, ...) value,
outputFunc = NULL, outputArgs = NULL
func,
transform = function(value, session, name, ...) value,
outputFunc = NULL,
outputArgs = NULL,
cacheHint = "auto",
cacheWriteHook = NULL,
cacheReadHook = NULL
) {
renderFunc <- function(shinysession, name, ...) {
hybrid_chain(
func(),
function(value, .visible) {
transform(setVisible(value, .visible), shinysession, name, ...)
function(value) {
transform(value, shinysession, name, ...)
}
)
}
if (!is.null(outputFunc))
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
else
renderFunc
# Hoist func's wrappedFunc attribute into renderFunc, so that when we pass
# renderFunc on to markRenderFunction, it is able to find the original user
# function.
attr(renderFunc, "wrappedFunc") <- attr(func, "wrappedFunc", exact = TRUE)
markRenderFunction(outputFunc, renderFunc, outputArgs, cacheHint,
cacheWriteHook, cacheReadHook)
}
useRenderFunction <- function(renderFunc, inline = FALSE) {
@@ -140,6 +331,22 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
useRenderFunction(x, inline = inline)
}
# Get relevant attributes from a render function object.
renderFunctionAttributes <- function(x) {
attrs <- c("outputFunc", "outputArgs", "hasExecuted", "cacheHint", "otelAttrs")
names(attrs) <- attrs
lapply(attrs, function(name) attr(x, name, exact = TRUE))
}
# Add a named list of attributes to an object
addAttributes <- function(x, attrs) {
nms <- names(attrs)
for (i in seq_along(attrs)) {
attr(x, nms[i]) <- attrs[[i]]
}
x
}
#' Mark a render function with attributes that will be used by the output
#'
@@ -188,13 +395,13 @@ markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
#' The corresponding HTML output tag should be `div` or `img` and have
#' the CSS class name `shiny-image-output`.
#'
#' @seealso For more details on how the images are generated, and how to control
#' @seealso
#' * For more details on how the images are generated, and how to control
#' the output, see [plotPNG()].
#' * Use [outputOptions()] to set general output options for an image output.
#'
#' @param expr An expression that returns a list.
#' @param env The environment in which to evaluate `expr`.
#' @param quoted Is `expr` a quoted expression (with `quote()`)? This
#' is useful if you want to save an expression in a variable.
#' @inheritParams renderUI
#' @param deleteFile Should the file in `func()$src` be deleted after
#' it is sent to the client browser? Generally speaking, if the image is a
#' temp file generated within `func`, then this should be `TRUE`;
@@ -273,9 +480,10 @@ markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
#'
#' shinyApp(ui, server)
#' }
renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
deleteFile, outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
renderImage <- function(expr, env = parent.frame(), quoted = FALSE,
deleteFile, outputArgs=list())
{
func <- installExprFunction(expr, "func", env, quoted, label = "renderImage")
# missing() must be used directly within the function with the given arg
if (missing(deleteFile)) {
@@ -324,7 +532,7 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
}
# If contentType not specified, autodetect based on extension
contentType <- imageinfo$contentType %OR% getContentType(imageinfo$src)
contentType <- imageinfo$contentType %||% getContentType(imageinfo$src)
# Extra values are everything in imageinfo except 'src' and 'contentType'
extra_attr <- imageinfo[!names(imageinfo) %in% c('src', 'contentType')]
@@ -333,7 +541,10 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
c(src = session$fileUrl(name, file=imageinfo$src, contentType=contentType),
extra_attr)
},
imageOutput, outputArgs)
imageOutput,
outputArgs,
cacheHint = FALSE
)
}
# TODO: If we ever take a dependency on fs, it'd be great to replace this with
@@ -394,19 +605,19 @@ isTemp <- function(path, tempDir = tempdir(), mustExist) {
#' function return [invisible()].
#'
#' @param expr An expression to evaluate.
#' @param env The environment in which to evaluate `expr`. For expert use only.
#' @param quoted Is `expr` a quoted expression (with `quote()`)? This
#' is useful if you want to save an expression in a variable.
#' @inheritParams renderUI
#' @param width Width of printed output.
#' @param outputArgs A list of arguments to be passed through to the implicit
#' call to [verbatimTextOutput()] or [textOutput()] when the functions are
#' used in an interactive RMarkdown document.
#'
#' @example res/text-example.R
#' @seealso [outputOptions()]
#' @export
renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
width = getOption('width'), outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
width = getOption('width'), outputArgs=list())
{
func <- installExprFunction(expr, "func", env, quoted, label = "renderPrint")
# Set a promise domain that sets the console width
# and captures output
@@ -417,14 +628,14 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
domain <- createRenderPrintPromiseDomain(width)
hybrid_chain(
{
promises::with_promise_domain(domain, func())
with_promise_domain(domain, func())
},
function(value, .visible) {
if (.visible) {
cat(file = domain$conn, paste(utils::capture.output(value, append = TRUE), collapse = "\n"))
function(value) {
res <- withVisible(value)
if (res$visible) {
cat(file = domain$conn, paste(utils::capture.output(res$value, append = TRUE), collapse = "\n"))
}
res <- paste(readLines(domain$conn, warn = FALSE), collapse = "\n")
res
paste(readLines(domain$conn, warn = FALSE), collapse = "\n")
},
finally = function() {
close(domain$conn)
@@ -432,13 +643,21 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
)
}
markRenderFunction(verbatimTextOutput, renderFunc, outputArgs = outputArgs)
markRenderFunction(
verbatimTextOutput,
renderFunc,
outputArgs,
cacheHint = list(
label = "renderPrint",
origUserExpr = installedFuncExpr(func)
)
)
}
createRenderPrintPromiseDomain <- function(width) {
f <- file()
promises::new_promise_domain(
new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
function(...) {
@@ -480,16 +699,18 @@ createRenderPrintPromiseDomain <- function(width) {
#' element.
#' @export
#' @rdname renderPrint
renderText <- function(expr, env=parent.frame(), quoted=FALSE,
renderText <- function(expr, env = parent.frame(), quoted = FALSE,
outputArgs=list(), sep=" ") {
installExprFunction(expr, "func", env, quoted)
func <- installExprFunction(expr, "func", env, quoted, label = "renderText")
createRenderFunction(
func,
function(value, session, name, ...) {
paste(utils::capture.output(cat(value, sep=sep)), collapse="\n")
},
textOutput, outputArgs
textOutput,
outputArgs
)
}
@@ -502,14 +723,18 @@ renderText <- function(expr, env=parent.frame(), quoted=FALSE,
#'
#' @param expr An expression that returns a Shiny tag object, [HTML()],
#' or a list of such objects.
#' @param env The environment in which to evaluate `expr`.
#' @param quoted Is `expr` a quoted expression (with `quote()`)? This
#' is useful if you want to save an expression in a variable.
#' @template param-env
#' @templateVar x expr
#' @templateVar env env
#' @templateVar quoted quoted
#' @template param-quoted
#' @templateVar x expr
#' @templateVar quoted quoted
#' @param outputArgs A list of arguments to be passed through to the implicit
#' call to [uiOutput()] when `renderUI` is used in an
#' interactive R Markdown document.
#'
#' @seealso [uiOutput()]
#' @seealso [uiOutput()], [outputOptions()]
#' @export
#' @examples
#' ## Only run examples in interactive R sessions
@@ -530,9 +755,10 @@ renderText <- function(expr, env=parent.frame(), quoted=FALSE,
#' shinyApp(ui, server)
#' }
#'
renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
renderUI <- function(expr, env = parent.frame(), quoted = FALSE,
outputArgs = list())
{
func <- installExprFunction(expr, "func", env, quoted, label = "renderUI")
createRenderFunction(
func,
@@ -542,7 +768,8 @@ renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
processDeps(result, shinysession)
},
uiOutput, outputArgs
uiOutput,
outputArgs
)
}
@@ -565,10 +792,10 @@ renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
#' that file path. (Reactive values and functions may be used from this
#' function.)
#' @param contentType A string of the download's
#' [content type](http://en.wikipedia.org/wiki/Internet_media_type), for
#' example `"text/csv"` or `"image/png"`. If `NULL` or
#' `NA`, the content type will be guessed based on the filename
#' extension, or `application/octet-stream` if the extension is unknown.
#' [content type](https://en.wikipedia.org/wiki/Internet_media_type), for
#' example `"text/csv"` or `"image/png"`. If `NULL`, the content type
#' will be guessed based on the filename extension, or
#' `application/octet-stream` if the extension is unknown.
#' @param outputArgs A list of arguments to be passed through to the implicit
#' call to [downloadButton()] when `downloadHandler` is used
#' in an interactive R Markdown document.
@@ -597,38 +824,50 @@ renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
#'
#' shinyApp(ui, server)
#' }
#'
#' @seealso
#' * The download handler, like other outputs, is suspended (disabled) by
#' default for download buttons and links that are hidden. Use
#' [outputOptions()] to control this behavior, e.g. to set
#' `suspendWhenHidden = FALSE` if the download is initiated by
#' programmatically clicking on the download button using JavaScript.
#' @export
downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()) {
downloadHandler <- function(filename, content, contentType=NULL, outputArgs=list()) {
renderFunc <- function(shinysession, name, ...) {
shinysession$registerDownload(name, filename, contentType, content)
}
snapshotExclude(
markRenderFunction(downloadButton, renderFunc, outputArgs = outputArgs)
markRenderFunction(downloadButton, renderFunc, outputArgs, cacheHint = FALSE)
)
}
#' Table output with the JavaScript library DataTables
#' Table output with the JavaScript DataTables library
#'
#' Makes a reactive version of the given function that returns a data frame (or
#' matrix), which will be rendered with the DataTables library. Paging,
#' searching, filtering, and sorting can be done on the R side using Shiny as
#' the server infrastructure.
#' @description
#' `r lifecycle::badge("deprecated")`
#'
#' This function is deprecated, use
#' [DT::renderDT()](https://rstudio.github.io/DT/shiny.html) instead. It
#' provides a superset of functionality, better performance, and better user
#' experience.
#'
#' For the `options` argument, the character elements that have the class
#' `"AsIs"` (usually returned from [base::I()]) will be evaluated in
#' JavaScript. This is useful when the type of the option value is not supported
#' in JSON, e.g., a JavaScript function, which can be obtained by evaluating a
#' character string. Note this only applies to the root-level elements of the
#' options list, and the `I()` notation does not work for lower-level
#' elements in the list.
#' @param expr An expression that returns a data frame or a matrix.
#' @inheritParams renderTable
#' @param options A list of initialization options to be passed to DataTables,
#' or a function to return such a list.
#' or a function to return such a list. You can find a complete list of
#' options at <https://datatables.net/reference/option/>.
#'
#' Any top-level strings with class `"AsIs"` (as created by [I()]) will be
#' evaluated in JavaScript. This is useful when the type of the option value
#' is not supported in JSON, e.g., a JavaScript function, which can be
#' obtained by evaluating a character string. This only applies to the
#' root-level elements of options list, and does not worked for lower-level
#' elements in the list.
#' @param searchDelay The delay for searching, in milliseconds (to avoid too
#' frequent search requests).
#' @param callback A JavaScript function to be applied to the DataTable object.
#' This is useful for DataTables plug-ins, which often require the DataTable
#' instance to be available (<http://datatables.net/extensions/>).
#' instance to be available.
#' @param escape Whether to escape HTML entities in the table: `TRUE` means
#' to escape the whole table, and `FALSE` means not to escape it.
#' Alternatively, you can specify numeric column indices or column names to
@@ -636,17 +875,8 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
#' `c(1, 3, 4)`, or `c(-1, -3)` (all columns except the first and
#' third), or `c('Species', 'Sepal.Length')`.
#' @param outputArgs A list of arguments to be passed through to the implicit
#' call to [dataTableOutput()] when `renderDataTable` is used
#' call to `dataTableOutput()` when `renderDataTable()` is used
#' in an interactive R Markdown document.
#'
#' @references <http://datatables.net>
#' @note This function only provides the server-side version of DataTables
#' (using R to process the data object on the server side). There is a
#' separate package \pkg{DT} (<https://github.com/rstudio/DT>) that allows
#' you to create both server-side and client-side DataTables, and supports
#' additional DataTables features. Consider using `DT::renderDataTable()`
#' and `DT::dataTableOutput()` (see
#' <http://rstudio.github.io/DT/shiny.html> for more information).
#' @export
#' @inheritParams renderPlot
#' @examples
@@ -671,11 +901,62 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
#' }
#' )
#' }
#' @keywords internal
renderDataTable <- function(expr, options = NULL, searchDelay = 500,
callback = 'function(oTable) {}', escape = TRUE,
env = parent.frame(), quoted = FALSE,
outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
outputArgs = list()) {
legacy <- useLegacyDataTable(
from = "shiny::renderDataTable()",
to = "DT::renderDT()"
)
if (!quoted) {
expr <- substitute(expr)
quoted <- TRUE
}
if (legacy) {
legacyRenderDataTable(
expr, env = env, quoted = quoted,
options = options,
searchDelay = searchDelay,
callback = callback,
escape = escape,
outputArgs = outputArgs
)
} else {
if (!missing(searchDelay)) {
warning("Ignoring renderDataTable()'s searchDelay value (since DT::renderDT() has no equivalent).")
}
force(options)
force(callback)
force(escape)
force(outputArgs)
DT::renderDataTable(
expr, env = env, quoted = quoted,
options = if (is.null(options)) list() else options,
# Turn function into a statement
callback = DT::JS(paste0("(", callback, ")(table)")),
escape = escape,
outputArgs = outputArgs
)
}
}
legacyRenderDataTable <- function(expr, options = NULL, searchDelay = 500,
callback = 'function(oTable) {}', escape = TRUE,
env = parent.frame(), quoted = FALSE,
outputArgs=list()) {
func <- installExprFunction(expr, "func", env, quoted, label = "renderDataTable")
renderFunc <- function(shinysession, name, ...) {
if (is.function(options)) options <- options()
@@ -709,7 +990,8 @@ renderDataTable <- function(expr, options = NULL, searchDelay = 500,
)
}
renderFunc <- markRenderFunction(dataTableOutput, renderFunc, outputArgs = outputArgs)
renderFunc <- markRenderFunction(dataTableOutput, renderFunc, outputArgs,
cacheHint = FALSE)
renderFunc <- snapshotPreprocessOutput(renderFunc, function(value) {
# Remove the action field so that it's not saved in test snapshots. It
@@ -727,7 +1009,7 @@ renderDataTable <- function(expr, options = NULL, searchDelay = 500,
DT10Names <- function() {
rbind(
utils::read.table(
system.file('www/shared/datatables/upgrade1.10.txt', package = 'shiny'),
system_file('www/shared/datatables/upgrade1.10.txt', package = 'shiny'),
stringsAsFactors = FALSE
),
c('aoColumns', 'Removed') # looks like an omission on the upgrade guide
@@ -762,64 +1044,3 @@ checkDT9 <- function(options) {
names(options)[i] <- nms10
options
}
# Deprecated functions ------------------------------------------------------
#' Deprecated reactive functions
#' @name deprecatedReactives
#' @keywords internal
NULL
#' Plot output (deprecated)
#'
#' `reactivePlot` has been replaced by [renderPlot()].
#' @param func A function.
#' @param width Width.
#' @param height Height.
#' @param ... Other arguments to pass on.
#' @rdname deprecatedReactives
#' @export
reactivePlot <- function(func, width='auto', height='auto', ...) {
shinyDeprecated(new="renderPlot")
renderPlot({ func() }, width=width, height=height, ...)
}
#' Table output (deprecated)
#'
#' `reactiveTable` has been replaced by [renderTable()].
#' @rdname deprecatedReactives
#' @export
reactiveTable <- function(func, ...) {
shinyDeprecated(new="renderTable")
renderTable({ func() })
}
#' Print output (deprecated)
#'
#' `reactivePrint` has been replaced by [renderPrint()].
#' @rdname deprecatedReactives
#' @export
reactivePrint <- function(func) {
shinyDeprecated(new="renderPrint")
renderPrint({ func() })
}
#' UI output (deprecated)
#'
#' `reactiveUI` has been replaced by [renderUI()].
#' @rdname deprecatedReactives
#' @export
reactiveUI <- function(func) {
shinyDeprecated(new="renderUI")
renderUI({ func() })
}
#' Text output (deprecated)
#'
#' `reactiveText` has been replaced by [renderText()].
#' @rdname deprecatedReactives
#' @export
reactiveText <- function(func) {
shinyDeprecated(new="renderText")
renderText({ func() })
}

View File

@@ -32,26 +32,34 @@ licenseLink <- function(licenseName) {
showcaseHead <- function() {
deps <- list(
htmlDependency("jqueryui", "1.12.1", c(href="shared/jqueryui"),
script = "jquery-ui.min.js"),
htmlDependency("showdown", "0.3.1", c(href="shared/showdown/compressed"),
script = "showdown.js"),
htmlDependency("highlight.js", "6.2", c(href="shared/highlight"),
script = "highlight.pack.js")
jqueryuiDependency(),
htmlDependency(
"highlight.js",
"6.2",
src = "www/shared/highlight",
package="shiny",
script = "highlight.pack.js",
stylesheet = "rstudio.css"
),
htmlDependency(
"showcase",
"0.1.0",
src = "www/shared",
package = "shiny",
script = "shiny-showcase.js",
stylesheet = "shiny-showcase.css",
all_files = FALSE
)
)
mdfile <- file.path.ci(getwd(), 'Readme.md')
html <- with(tags, tagList(
script(src="shared/shiny-showcase.js"),
link(rel="stylesheet", type="text/css",
href="shared/highlight/rstudio.css"),
link(rel="stylesheet", type="text/css",
href="shared/shiny-showcase.css"),
if (file.exists(mdfile))
script(type="text/markdown", id="showcase-markdown-content",
paste(readUTF8(mdfile), collapse="\n"))
else ""
))
html <- tagList(
if (file.exists(mdfile)) {
md_content <- paste(readUTF8(mdfile), collapse="\n")
md_html <- commonmark::markdown_html(md_content, extensions = TRUE)
tags$template(id="showcase-markdown-content", HTML(md_html))
} else ""
)
return(attachDependencies(html, deps))
}
@@ -83,7 +91,7 @@ navTabsHelper <- function(files, prefix = "") {
with(tags,
li(class=if (tolower(file) %in% c("app.r", "server.r")) "active" else "",
a(href=paste("#", gsub(".", "_", file, fixed=TRUE), "_code", sep=""),
"data-toggle"="tab", paste0(prefix, file)))
"data-toggle"="tab", "data-bs-toggle"="tab", paste0(prefix, file)))
)
})
}
@@ -92,7 +100,7 @@ navTabsDropdown <- function(files) {
if (length(files) > 0) {
with(tags,
li(role="presentation", class="dropdown",
a(class="dropdown-toggle", `data-toggle`="dropdown", href="#",
a(class="dropdown-toggle", `data-toggle`="dropdown", `data-bs-toggle`="dropdown", href="#",
role="button", `aria-haspopup`="true", `aria-expanded`="false",
"www", span(class="caret")
),
@@ -134,7 +142,7 @@ showcaseCodeTabs <- function(codeLicense) {
a(id="showcase-code-position-toggle",
class="btn btn-default btn-sm",
onclick="toggleCodePosition()",
icon("level-up"),
icon("level-up-alt"),
"show with app"),
ul(class="nav nav-tabs",
navTabsHelper(rFiles),
@@ -220,4 +228,3 @@ showcaseUI <- function(ui) {
showcaseBody(ui)
)
}

View File

@@ -1,70 +0,0 @@
# A Stack object backed by a list. The backing list will grow or shrink as
# the stack changes in size.
Stack <- R6Class(
'Stack',
portable = FALSE,
class = FALSE,
public = list(
initialize = function(init = 20L) {
# init is the initial size of the list. It is also used as the minimum
# size of the list as it shrinks.
private$stack <- vector("list", init)
private$init <- init
},
push = function(..., .list = NULL) {
args <- c(list(...), .list)
new_size <- count + length(args)
# Grow if needed; double in size
while (new_size > length(stack)) {
stack[length(stack) * 2] <<- list(NULL)
}
stack[count + seq_along(args)] <<- args
count <<- new_size
invisible(self)
},
pop = function() {
if (count == 0L)
return(NULL)
value <- stack[[count]]
stack[count] <<- list(NULL)
count <<- count - 1L
# Shrink list if < 1/4 of the list is used, down to a minimum size of `init`
len <- length(stack)
if (len > init && count < len/4) {
new_len <- max(init, ceiling(len/2))
stack <<- stack[seq_len(new_len)]
}
value
},
peek = function() {
if (count == 0L)
return(NULL)
stack[[count]]
},
size = function() {
count
},
# Return the entire stack as a list, where the first item in the list is the
# oldest item in the stack, and the last item is the most recently added.
as_list = function() {
stack[seq_len(count)]
}
),
private = list(
stack = NULL, # A list that holds the items
count = 0L, # Current number of items in the stack
init = 20L # Initial and minimum size of the stack
)
)

200
R/staticimports.R Normal file
View File

@@ -0,0 +1,200 @@
# Generated by staticimports; do not edit by hand.
# ======================================================================
# Imported from pkg:staticimports
# ======================================================================
# Given a vector, return TRUE if any elements are named, FALSE otherwise.
# For zero-length vectors, always return FALSE.
any_named <- function(x) {
if (length(x) == 0) return(FALSE)
nms <- names(x)
!is.null(nms) && any(nzchar(nms))
}
# Given a vector, return TRUE if any elements are unnamed, FALSE otherwise.
# For zero-length vectors, always return FALSE.
any_unnamed <- function(x) {
if (length(x) == 0) return(FALSE)
nms <- names(x)
is.null(nms) || !all(nzchar(nms))
}
# Borrowed from pkgload:::dev_meta, with some modifications.
# Returns TRUE if `pkg` was loaded with `devtools::load_all()`.
devtools_loaded <- function(pkg) {
ns <- .getNamespace(pkg)
if (is.null(ns) || is.null(ns$.__DEVTOOLS__)) {
return(FALSE)
}
TRUE
}
get_package_version <- function(pkg) {
# `utils::packageVersion()` can be slow, so first try the fast path of
# checking if the package is already loaded.
ns <- .getNamespace(pkg)
if (is.null(ns)) {
utils::packageVersion(pkg)
} else {
as.package_version(ns$.__NAMESPACE__.$spec[["version"]])
}
}
is_installed <- function(pkg, version = NULL) {
installed <- isNamespaceLoaded(pkg) || nzchar(system_file_cached(package = pkg))
if (is.null(version)) {
return(installed)
}
if (!is.character(version) && !inherits(version, "numeric_version")) {
# Avoid https://bugs.r-project.org/show_bug.cgi?id=18548
alert <- if (identical(Sys.getenv("TESTTHAT"), "true")) stop else warning
alert("`version` must be a character string or a `package_version` or `numeric_version` object.")
version <- numeric_version(sprintf("%0.9g", version))
}
installed && isTRUE(get_package_version(pkg) >= version)
}
# Simplified version rlang:::s3_register() that just uses
# warning() instead of rlang::warn() when registration fails
# https://github.com/r-lib/rlang/blob/main/R/compat-s3-register.R
s3_register <- function(generic, class, method = NULL) {
stopifnot(is.character(generic), length(generic) == 1)
stopifnot(is.character(class), length(class) == 1)
pieces <- strsplit(generic, "::")[[1]]
stopifnot(length(pieces) == 2)
package <- pieces[[1]]
generic <- pieces[[2]]
caller <- parent.frame()
get_method_env <- function() {
top <- topenv(caller)
if (isNamespace(top)) {
asNamespace(environmentName(top))
} else {
caller
}
}
get_method <- function(method, env) {
if (is.null(method)) {
get(paste0(generic, ".", class), envir = get_method_env())
} else {
method
}
}
register <- function(...) {
envir <- asNamespace(package)
# Refresh the method each time, it might have been updated by
# `devtools::load_all()`
method_fn <- get_method(method)
stopifnot(is.function(method_fn))
# Only register if generic can be accessed
if (exists(generic, envir)) {
registerS3method(generic, class, method_fn, envir = envir)
} else {
warning(
"Can't find generic `", generic, "` in package ", package,
" register S3 method. Do you need to update ", package,
" to the latest version?", call. = FALSE
)
}
}
# Always register hook in case package is later unloaded & reloaded
setHook(packageEvent(package, "onLoad"), function(...) {
register()
})
# Avoid registration failures during loading (pkgload or regular).
# Check that environment is locked because the registering package
# might be a dependency of the package that exports the generic. In
# that case, the exports (and the generic) might not be populated
# yet (#1225).
if (isNamespaceLoaded(package) && environmentIsLocked(asNamespace(package))) {
register()
}
invisible()
}
# Borrowed from pkgload::shim_system.file, with some modifications. This behaves
# like `system.file()`, except that (1) for packages loaded with
# `devtools::load_all()`, it will return the path to files in the package's
# inst/ directory, and (2) for other packages, the directory lookup is cached.
# Also, to keep the implementation simple, it doesn't support specification of
# lib.loc or mustWork.
system_file <- function(..., package = "base") {
if (!devtools_loaded(package)) {
return(system_file_cached(..., package = package))
}
if (!is.null(names(list(...)))) {
stop("All arguments other than `package` must be unnamed.")
}
# If package was loaded with devtools (the package loaded with load_all),
# also search for files under inst/, and don't cache the results (it seems
# more likely that the package path will change during the development
# process)
pkg_path <- find.package(package)
# First look in inst/
files_inst <- file.path(pkg_path, "inst", ...)
present_inst <- file.exists(files_inst)
# For any files that weren't present in inst/, look in the base path
files_top <- file.path(pkg_path, ...)
present_top <- file.exists(files_top)
# Merge them together. Here are the different possible conditions, and the
# desired result. NULL means to drop that element from the result.
#
# files_inst: /inst/A /inst/B /inst/C /inst/D
# present_inst: T T F F
# files_top: /A /B /C /D
# present_top: T F T F
# result: /inst/A /inst/B /C NULL
#
files <- files_top
files[present_inst] <- files_inst[present_inst]
# Drop cases where not present in either location
files <- files[present_inst | present_top]
if (length(files) == 0) {
return("")
}
# Make sure backslashes are replaced with slashes on Windows
normalizePath(files, winslash = "/")
}
# A wrapper for `system.file()`, which caches the package path because
# `system.file()` can be slow. If a package is not installed, the result won't
# be cached.
system_file_cached <- local({
pkg_dir_cache <- character()
function(..., package = "base") {
if (!is.null(names(list(...)))) {
stop("All arguments other than `package` must be unnamed.")
}
not_cached <- is.na(match(package, names(pkg_dir_cache)))
if (not_cached) {
pkg_dir <- system.file(package = package)
if (nzchar(pkg_dir)) {
pkg_dir_cache[[package]] <<- pkg_dir
}
} else {
pkg_dir <- pkg_dir_cache[[package]]
}
file.path(pkg_dir, ...)
}
})

View File

@@ -1,19 +1,14 @@
#' @noRd
isModuleServer <- function(x) {
is.function(x) && names(formals(x))[1] == "id"
}
#' Reactive testing for Shiny server functions and modules
#'
#' A way to test the reactive interactions in Shiny applications. Reactive
#' interactions are defined in the server function of applications and in
#' modules.
#' @param app The path to an application or module to test. In addition to
#' paths, applications may be represented by any object suitable for coercion
#' to an `appObj` by [`as.shiny.appobj`]. Application server functions must
#' include a `session` argument in order to be tested. If `app` is `NULL` or
#' not supplied, the nearest enclosing directory that is a Shiny app, starting
#' with the current directory, is used.
#' @param app A server function (i.e. a function with `input`, `output`,
#' and `session`), or a module function (i.e. a function with first
#' argument `id` that calls [moduleServer()].
#'
#' You can also provide an app, a path an app, or anything that
#' [`as.shiny.appobj()`] can handle.
#' @param expr Test code containing expectations. The objects from inside the
#' server function environment will be made available in the environment of
#' the test expression (this is done using a data mask with
@@ -30,7 +25,19 @@ isModuleServer <- function(x) {
#' @include mock-session.R
#' @rdname testServer
#' @examples
#' server <- function(id, multiplier = 2, prefix = "I am ") {
#' # Testing a server function ----------------------------------------------
#' server <- function(input, output, session) {
#' x <- reactive(input$a * input$b)
#' }
#'
#' testServer(server, {
#' session$setInputs(a = 2, b = 3)
#' stopifnot(x() == 6)
#' })
#'
#'
#' # Testing a module --------------------------------------------------------
#' myModuleServer <- function(id, multiplier = 2, prefix = "I am ") {
#' moduleServer(id, function(input, output, session) {
#' myreactive <- reactive({
#' input$x * multiplier
@@ -41,7 +48,7 @@ isModuleServer <- function(x) {
#' })
#' }
#'
#' testServer(server, args = list(multiplier = 2), {
#' testServer(myModuleServer, args = list(multiplier = 2), {
#' session$setInputs(x = 1)
#' # You're also free to use third-party
#' # testing packages like testthat:
@@ -56,74 +63,94 @@ isModuleServer <- function(x) {
#' })
#' @export
testServer <- function(app = NULL, expr, args = list(), session = MockShinySession$new()) {
require(shiny)
if (!is.null(getDefaultReactiveDomain()))
stop("testServer() is for use only within tests and may not indirectly call itself.")
on.exit(if (!session$isClosed()) session$close(), add = TRUE)
quosure <- rlang::enquo(expr)
on.exit(if (!session$isClosed()) session$close())
withMockContext <- function(expr) {
isolate(
withReactiveDomain(session, {
withr::with_options(list(`shiny.allowoutputreads` = TRUE), {
withLocalOptions({
# Sets a cache for renderCachedPlot() with cache = "app" to use.
shinyOptions("cache" = session$appcache)
expr
})
})
})
)
}
if (isModuleServer(app)) {
if (!("id" %in% names(args)))
args[["id"]] <- session$genId()
# app is presumed to be a module, and modules may take additional arguments,
# so splice in any args.
withMockContext(rlang::exec(app, !!!args))
withMockContext(session, rlang::exec(app, !!!args))
# If app is a module, then we must use both the module function's immediate
# environment and also its enclosing environment to construct the mask.
parent_clone <- rlang::env_clone(parent.env(session$env))
clone <- rlang::env_clone(session$env, parent_clone)
mask <- rlang::new_data_mask(clone, parent_clone)
withMockContext(rlang::eval_tidy(quosure, mask, rlang::caller_env()))
} else {
if (is.null(app)) {
app <- findEnclosingApp(".")
}
appobj <- as.shiny.appobj(app)
if (!is.null(appobj$onStart))
appobj$onStart()
# Ensure appobj$onStop() is called, and the current directory is restored,
# regardless of whether invoking the server function is successful.
tryCatch({
server <- appobj$serverFuncSource()
if (! "session" %in% names(formals(server)))
stop("Tested application server functions must declare input, output, and session arguments.")
body(server) <- rlang::expr({
session$setEnv(base::environment())
!!body(server)
})
if (length(args))
stop("Arguments were provided to a server function.")
withMockContext(server(input = session$input, output = session$output, session = session))
}, finally = {
if (!is.null(appobj$onStop))
appobj$onStop()
})
# If app is a server, we use only the server function's immediate
# environment to construct the mask.
mask <- rlang::new_data_mask(rlang::env_clone(session$env))
withMockContext(rlang::eval_tidy(quosure, mask, rlang::caller_env()))
withMockContext(session, rlang::eval_tidy(quosure, mask, rlang::caller_env()))
return(invisible())
}
if (is.null(app)) {
path <- findEnclosingApp(".")
app <- shinyAppDir(path)
} else if (isServer(app)) {
app <- shinyApp(fluidPage(), app)
} else {
app <- as.shiny.appobj(app)
}
if (!is.null(app$onStart))
app$onStart()
if (!is.null(app$onStop))
on.exit(app$onStop(), add = TRUE)
server <- app$serverFuncSource()
if (!"session" %in% names(formals(server)))
stop("Tested application server functions must declare input, output, and session arguments.")
if (length(args))
stop("Arguments were provided to a server function.")
body(server) <- rlang::expr({
session$setEnv(base::environment())
!!body(server)
})
withMockContext(session,
server(input = session$input, output = session$output, session = session)
)
# # If app is a server, we use only the server function's immediate
# # environment to construct the mask.
mask <- rlang::new_data_mask(rlang::env_clone(session$env))
withMockContext(session, {
rlang::eval_tidy(quosure, mask, rlang::caller_env())
})
invisible()
}
withMockContext <- function(session, expr) {
isolate(
withReactiveDomain(session, {
withr::with_options(list(`shiny.allowoutputreads` = TRUE), {
# Sets a cache for renderCachedPlot() with cache = "app" to use.
shinyOptions("cache" = session$appcache)
expr
})
})
)
}
# Helpers -----------------------------------------------------------------
isModuleServer <- function(x) {
is.function(x) && names(formals(x))[[1]] == "id"
}
isServer <- function(x) {
if (!is.function(x)) {
return(FALSE)
}
if (length(formals(x)) < 3) {
return(FALSE)
}
identical(names(formals(x))[1:3], c("input", "output", "session"))
}

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