Compare commits

...

1092 Commits

Author SHA1 Message Date
cpsievert
0dade0c553 Document (GitHub Actions) 2021-03-22 17:45:21 +00:00
Carson
ad6c1a4660 Revert "remove card argument (at least for now)"
This reverts commit b5b0ba1774.
2021-03-22 12:39:38 -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
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
23dbb0b41c Unified eol in Gruntfile.js for all the platforms (#3001)
* Unified eol char for all the platforms

* Reran yarn build after merging master
2020-08-13 16:54:44 -04:00
Winston Chang
c72ae68de5 Merge pull request #2993 from jooyoungseo/selectize-a11y 2020-08-13 13:56:39 -05:00
JooYoung Seo
7c1f87aed3 Rebuilt via WSL 2020-08-12 19:20:42 -04:00
JooYoung Seo
6ec0ac1651 Reran yarn after updating grunt 2020-08-12 16:34:08 -04:00
JooYoung Seo
9dc84e5c2b Merge branch 'master' into selectize-a11y 2020-08-12 15:24:29 -04:00
JooYoung Seo
1e17b55f49 Got rid of es5 info and corrected selectize version 2020-08-12 15:24: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
JooYoung Seo
e2537d8e93 Used withr for applying datepicker patches 2020-08-12 12:56:13 -04:00
JooYoung Seo
6d35cb6c77 yarn grunt instead of yarn build 2020-08-07 11:20:27 -04:00
Carson Sievert
8ce7f64679 Merge pull request #2995 from jooyoungseo/IonRangeSlider-updateScript
Tweaked updateIonRangeSlider.R
2020-08-07 09:40:42 -05:00
JooYoung Seo
5cd6250f05 Updated NEWS.md 2020-08-06 17:17:08 -04:00
JooYoung Seo
4872cd91a5 Reran yarn build 2020-08-06 17:00:54 -04:00
JooYoung Seo
08e84e2ca0 Tweaked updateSelectize.R 2020-08-06 16:22:47 -04:00
JooYoung Seo
40889c9637 Used withr when applying patches and doing yarn build 2020-08-06 16:12:11 -04:00
JooYoung Seo
010ba6f214 Update tools/updateIonRangeSlider.R
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2020-08-06 15:12:26 -04:00
JooYoung Seo
2f8dc860ff Update tools/updateIonRangeSlider.R
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2020-08-06 15:11:55 -04:00
JooYoung Seo
88f61f0d45 Update tools/updateIonRangeSlider.R
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2020-08-06 15:11:37 -04:00
JooYoung Seo
16a5aa7489 Update tools/updateIonRangeSlider.R
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2020-08-06 15:11:19 -04:00
JooYoung Seo
89fd2b2ed3 Tweaked updateIonRangeSlider.R 2020-08-06 13:14:11 -04:00
JooYoung Seo
a16769061e Removed es5 shim 2020-08-05 20:31:33 -04:00
JooYoung Seo
3be76145b7 Updated grunt file to minify selectize-plugin-a11y 2020-08-05 20:23:23 -04:00
JooYoung Seo
e9d27fa632 Update tools/updateSelectize.R
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2020-08-05 17:36:16 -04:00
JooYoung Seo
be706e4bb7 Added updating scripts and LICENSE info 2020-08-05 17:15:02 -04:00
JooYoung Seo
b87c4d5623 Appended a11y plugin to options 2020-08-05 11:33:58 -04:00
JooYoung Seo
03a260f26a Defaulting a11y plugins 2020-08-04 22:32:11 -04:00
JooYoung Seo
ae3e007a5f Updated selectize-plugin-a11y 2020-08-04 16:58:23 -04:00
JooYoung Seo
cfbf76d898 Added selectize-plugin-a11y 2020-08-04 14:45:34 -04:00
Carson Sievert
0b82b121cb Merge pull request #2978 from jooyoungseo/dateInput-a11y
* Fixed #2951: Added labels and title for dateInput and dateRangeInput
2020-08-03 09:48:46 -05:00
JooYoung Seo
643ebb4946 Merge branch 'master' of https://github.com/rstudio/shiny into dateInput-a11y 2020-07-31 17:12:40 -04:00
Carson
8693eed3ec use expect_equal() instead of grepl() in expectation 2020-07-31 15:05:05 -05:00
Winston Chang
766b910150 Add info about calling registerInputHandler from .onLoad 2020-07-28 13:58:36 -05:00
Winston Chang
545843ffe6 Fix pkgdown.yml 2020-07-28 12:59:39 -05:00
Winston Chang
39e7b23d5b Merge pull request #2934 from hadley/render-text-print 2020-07-28 11:15:15 -05:00
Hadley Wickham
9d0db6f74c Remove renderPrint from pkgdown index 2020-07-27 15:37:39 -05:00
Winston Chang
78fb25329c NEWS edits 2020-07-27 14:39:37 -05:00
Winston Chang
7db6a7b57a Update NEWS 2020-07-27 14:38:00 -05:00
Winston Chang
76f70179c0 Merge pull request #2982 from ColinFay/patch-2 2020-07-27 14:36:50 -05:00
Winston Chang
d6aecfe9ae Update NEWS 2020-07-27 14:28:13 -05:00
Winston Chang
1354d3dec1 Merge pull request #2981 from ColinFay/patch-1 2020-07-27 14:26:36 -05:00
colin
955ae817d8 switched from * to + in the regex that checks resource path 2020-07-27 21:20:03 +02:00
colin
aee9589c1a Simple regex in addResourcePath 2020-07-25 22:42:09 +02:00
colin
48ac0f55c3 check that the prefix is not only made of dots 2020-07-25 22:38:41 +02:00
colin
831c0a340c redoc the runExample() 2020-07-24 11:46:54 +02:00
Colin Fay
210d297d18 Update server.R 2020-07-24 11:08:28 +02:00
Colin Fay
afbcf9039e Allow to pass resource path that starts with .
This will close #2980
2020-07-24 09:33:52 +02:00
JooYoung Seo
e8eadc1a09 Updated test 2020-07-23 11:11:02 -04:00
JooYoung Seo
f234b7015c Date format instead of format 2020-07-22 16:49:22 -04:00
JooYoung Seo
8de38b3415 Updated NEWS.md 2020-07-22 13:22:45 -04:00
JooYoung Seo
0f132fc180 * Fixed #2951: Added labels and title for dateInput and dateRangeInput 2020-07-22 13:10:45 -04:00
Hadley Wickham
e597c24f35 Merged upstream/master into hadley-render-text-print 2020-07-19 12:59:01 -05: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
Winston Chang
abc6a98d0f Merge pull request #2974 from jooyoungseo/news-patch 2020-07-16 18:10:59 -05:00
JooYoung Seo
9415e79ff3 Merge branch 'master' of https://github.com/rstudio/shiny into news-patch 2020-07-16 16:47:56 -04:00
JooYoung Seo
6269022536 Followed convention for NEWS.md 2020-07-16 16:47:43 -04:00
Joe Cheng
6ad2125ee7 Calling runApp("app.R") ignored options passed into shinyApp() (#2969)
* Calling `runApp("app.R")` ignored options passed into `shinyApp()`

Fixes #1942.

This was caused by shinyAppDir_appR (which was called by shinyAppDir
and shinyAppFile, which were called when runApp() was used with either
no args or a path) dropping shinyApp() options on the floor.

Fixing this was not as simple as not dropping those options. Before
this commit, the app.R file was not invoked until after the host and
port options (possibly others) were already finalized. This commit
changes the effective sequence of events during startup.

- Calling shinyAppFile() or shinyAppDir() that points to a single-file
  app, now causes the app.R file to be sourced immediately; previously,
  sourcing would happen only at onStart(). (Honestly, the new behavior
  seems less surprising anyway.)

- The support files (R/*.R) for app.R were sourced during onStart. I've
  moved this so that the support files are sourced right before app.R
  is, and re-sourced every time app.R reloads.

* Code review feedback

* Code review feedback: improve test

* Roxygenize
2020-07-16 13:46:32 -07:00
JooYoung Seo
f5d7523a4f Merge pull request #2937 from jooyoungseo/fileInput-a11y
Improved fileInput keyboard accessibility
2020-07-16 16:16:55 -04:00
JooYoung Seo
ce31b9af7e Removed an issue URL from NEWS.md 2020-07-16 16:13:21 -04:00
JooYoung Seo
fc5d980a52 Resolved NEWS.md conflict 2020-07-16 15:43:35 -04:00
JooYoung Seo
9ea726732a Merge branch 'master' of https://github.com/rstudio/shiny 2020-07-16 15:40:10 -04:00
JooYoung Seo
db5f9cca73 Added Bootstrap accessibility plugin (#2911)
Co-authored-by: Winston Chang <winston@stdout.org>
2020-07-16 14:26:11 -05:00
JooYoung Seo
06fb4f6972 Merge branch 'master' of https://github.com/rstudio/shiny 2020-07-16 15:25:28 -04:00
Winston Chang
f045f9cf1b Merge pull request #2970 from rstudio/joe/feature/ui-http-response-2 2020-07-16 14:11:37 -05:00
Winston Chang
1752f57c7d Remove html5shiv and respond.js 2020-07-16 13:56:00 -05:00
Joe Cheng
7eb4bc15b8 Use soft breaks in NEWS 2020-07-16 10:09:43 -07:00
Joe Cheng
707b5ea851 Avoid spurious use of connections 2020-07-16 10:08:51 -07:00
Joe Cheng
fe9f679051 on.exit(add = TRUE)
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2020-07-16 10:03:43 -07:00
Joe Cheng
368a49be36 Add PR number to NEWS 2020-07-15 15:53:34 -07:00
Joe Cheng
80f0c5f5d7 Allow function-based UI to respond to non-GET requests 2020-07-15 15:51:36 -07:00
Joe Cheng
a5a7224228 Allow function-based UI to return a complete HTTP response
This allows the UI handler to have total control over the response,
including status code (for redirects) or content type (for serving
up files).
2020-07-15 14:36:54 -07:00
JooYoung Seo
d616cf045b Resolved NEWS.md 2020-07-15 11:17:52 -04:00
JooYoung Seo
8186ae060d Merge branch 'master' of https://github.com/rstudio/shiny 2020-07-15 11:12:04 -04:00
Carson Sievert
c46e80c711 Merge pull request #2941 from rstudio/showtext-fix
Include the pixelratio when setting showtext dpi
2020-07-14 13:53:22 -05:00
Carson
0735ebd7a8 update news 2020-07-14 13:52:18 -05:00
Carson
391bbaa73b Do the same when replaying plot 2020-07-14 13:45:34 -05:00
Carson
fed96c0e45 Include the pixelratio when setting showtext dpi 2020-07-14 13:45:34 -05:00
JooYoung Seo
f97f89a371 Merge branch 'master' of https://github.com/rstudio/shiny 2020-07-10 13:32:21 -04:00
Winston Chang
6352a5322b Rebuild documentation 2020-07-10 11:01:51 -05:00
JooYoung Seo
3473427484 Merge branch 'master' of https://github.com/rstudio/shiny 2020-07-10 11:30:59 -04:00
Winston Chang
d6c1733f0c Merge pull request #2917 from jooyoungseo/icon-a11y 2020-07-10 10:27:50 -05:00
JooYoung Seo
d641ac197b Merged recent shiny release 2020-07-10 11:16:10 -04:00
JooYoung Seo
6a0e41b05e Updated test-bootstrap.R 2020-07-10 11:09:44 -04:00
Winston Chang
e21a9a095e Merge pull request #2961 from rstudio/joe/misc/yarn-upgrade 2020-07-08 12:32:39 -05:00
Joe Cheng
56e1a0b939 Further dep upgrades 2020-07-03 09:23:52 -07:00
Joe Cheng
f9f9127a64 Build JS 2020-07-03 09:21:23 -07:00
Joe Cheng
e0628c4ae3 Upgrade build tools; fix Gruntfile.js for Windows 2020-07-03 09:17:37 -07:00
JooYoung Seo
43be342dea Merge branch 'master' of https://github.com/rstudio/shiny 2020-07-02 12:05:27 -04:00
Winston Chang
9cc7419700 Clarify how testServer expression is evaluated 2020-07-01 15:16:46 -05:00
Winston Chang
f559caf4d0 Bump version to 1.5.0.9000 2020-07-01 15:16:46 -05: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
41e42b8a53 Improved fileInput keyboard accessibility 2020-06-24 13:28:02 -04:00
JooYoung Seo
f6f5fbd6fb Merge branch 'master' of https://github.com/rstudio/shiny 2020-06-24 13:09:11 -04:00
Hadley Wickham
c7618e3991 Combine documentation of renderPrint() and renderText()
Since they're so closely related, and it makes it easier to see how they differ.
2020-06-23 08:13:55 -05:00
Winston Chang
da6df5da9e Rebuild docs with dev version of roxygen
This is to work around errors in R CMD check with R-devel:
  Non-file package-anchored link(s) in documentation object
2020-06-19 12:15:55 -05:00
Winston Chang
480cc79de4 Merge branch 'master' into rc-v1.5.0 2020-06-19 11:45:14 -05:00
Barret Schloerke
231c13d9a5 run revdep. updates redep_cran notes 2020-06-19 11:09:15 -04:00
Winston Chang
000406ec0b Remove Remotes field 2020-06-18 15:14:11 -05:00
Winston Chang
44e0a8bcb2 Remove links from NEWS.md
The links can cause R CMD check --as-cran to fail, because of rate-limiting by GitHub.
2020-06-18 14:29:35 -05:00
Winston Chang
d2e88c7a2f Refer to shinytest migration function 2020-06-17 10:46:36 -05:00
JooYoung Seo
638cddcd5e Merge branch 'master' of https://github.com/rstudio/shiny 2020-06-11 16:33:21 -04:00
JooYoung Seo
a3924f4ab1 Updated aria code and NEWS.md 2020-06-11 16:23:57 -04:00
JooYoung Seo
5798c396ec Fixed an incorrect line in tools/README.md for IonRangeSlider 2020-06-11 15:07:43 -05:00
JooYoung Seo
b1983f0a83 Fixed an incorrect line in tools/README.md for IonRangeSlider 2020-06-11 15:51:31 -04:00
JooYoung Seo
aca9f562e1 Provided auto-label for icon() instead of aria-hidden 2020-06-11 11:51:31 -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
Winston Chang
1e5051ef79 Merge pull request #2918 from ginberg/master
replace devtools by remotes
2020-06-05 09:42:53 -05:00
ginberg
d293dbc10f replace devtools by remotes 2020-06-05 15:26:07 +02:00
JooYoung Seo
703f481a9a Improved accessibility for icon() 2020-06-04 16:45:04 -04:00
Winston Chang
68f0c12cab Merge pull request #2902 from rstudio/alan-newsmd 2020-05-20 17:39:33 -05:00
Alan Dipert
8c7598f45d Drop testModule() from NEWS.md, fixes #2901 2020-05-20 22:33:02 +00:00
Barret Schloerke
192c4f239e bump version to 1.5.0. Build JS 2020-05-18 16:38:55 -05:00
Carson Sievert
64e09315fc Merge pull request #2900 from rstudio/jquery-3.5.1
Bump jQuery 3.5.0 to 3.5.1
2020-05-18 16:38:22 -05:00
Carson
db0c4155b8 news update 2020-05-18 16:28:36 -05:00
Carson
f971bfd80a Finish updating to jQuery 3.5.1, add a tools script, add documentation to tools/README.md 2020-05-18 16:25:40 -05:00
Carson
948244b45c Revert "Fixes #2896 by avoiding fatal JS exception in some collapsible navbar scenarios by patching a BS3 bug introduced https://github.com/twbs/bootstrap/pull/16011"
This reverts commit 4ba02c97a7.
2020-05-18 15:47:01 -05:00
Carson
c7fecbed7a bump jQuery 3.5.0 -> 3.5.1 2020-05-18 15:46:10 -05:00
Carson Sievert
f22cae98ef Merge pull request #2897 from rstudio/fix-collapsible-navbar
Fix collapsible navbar
2020-05-15 14:02:13 -05:00
Carson
4ba02c97a7 Fixes #2896 by avoiding fatal JS exception in some collapsible navbar scenarios by patching a BS3 bug introduced https://github.com/twbs/bootstrap/pull/16011 2020-05-15 12:38:24 -05:00
Winston Chang
0581dc7763 Remove old manual test app. Closes #2892 2020-05-12 16:30:24 -05:00
Winston Chang
77261d4872 Merge pull request #2891 from rstudio/wch-update-fontawesome 2020-05-12 16:26:10 -05:00
Winston Chang
cfd14ef169 Update NEWS 2020-05-12 16:25:12 -05:00
Winston Chang
c7cc76b044 Update tools/README.md
Co-authored-by: Barret Schloerke <barret@rstudio.com>
2020-05-12 16:16:57 -05:00
Winston Chang
44e1096753 Update README 2020-05-12 16:12:47 -05:00
Winston Chang
dc1c48ad4e Rebuild docs 2020-05-12 16:12:02 -05:00
Winston Chang
d9d29220cc Update NEWS 2020-05-12 16:04:05 -05:00
Winston Chang
6f744ef311 Update Font-Awesome to 5.13.0 2020-05-12 16:04:05 -05:00
Winston Chang
f7071f2231 Add updateFontAwesome script 2020-05-12 16:04:05 -05:00
Alan Dipert
ef75c9a35f Move callModule() to separate .Rd; add various links to docs (#2889)
* Move callModule() to separate .Rd; add various links to docs

* Remove callModule param from moduleServer docs

* document

* Update R/modules.R

* Rebuild docs

* Add callModule to pkgdown

Co-authored-by: Winston Chang <winston@stdout.org>
2020-05-12 12:46:41 -07:00
Winston Chang
4ca3c6c96a Clearer language for shinyAppTemplate prompt 2020-05-12 11:33:17 -05:00
Joe Cheng
82e98410ed isTempPath is now isTemp, simplified semantics (#2886)
* isTempPath is now isTemp, simplified semantics

- Now requires both the path and temp dir to exist
- isTemp(tempdir()) is now false
- Doesn't matter if it's files or directories

* Fix tests under R 3.4 and earlier

* fix comment

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2020-05-11 20:05:12 -07:00
Winston Chang
dcd92f03db Merge pull request #2867 from rstudio/alan-mocksession-methods 2020-05-11 21:33:54 -05:00
Alan Dipert
ba6c747e55 Merge remote-tracking branch 'origin/master' into alan-mocksession-methods 2020-05-11 21:47:06 +00:00
Alan Dipert
b53b766ee5 Add reactive/promise test 2020-05-11 21:30:12 +00:00
Barret Schloerke
cd737fccb5 Run pre-revdepcheck for v1.5.0. Found 2 errors. Made 2 PRs (#2885) 2020-05-11 17:06:06 -04:00
Alan Dipert
7fb3acea96 get() => !!as.symbol() 2020-05-11 21:00:50 +00:00
Alan Dipert
867052f974 Fix new CMD check note 2020-05-11 20:49:36 +00:00
Alan Dipert
038e010819 testServer() doc update 2020-05-11 20:34:40 +00:00
Alan Dipert
7f5e42fdd5 Fix pkgdown test 2020-05-11 19:46:11 +00:00
Alan Dipert
2fe9b3dcbe Fix tempdir()-related problem 2020-05-11 18:55:28 +00:00
Winston Chang
89dbfcecbf Ensure file encoding is specified for sort_c 2020-05-11 13:42:10 -05:00
Alan Dipert
f4bda6b91f Ensure temp files passed to downloadHandler() content functions are deleted on session close 2020-05-11 18:28:39 +00:00
Alan Dipert
dba72ac8a7 Call session ended callbacks, fix related existing/broken test 2020-05-11 18:20:37 +00:00
Barret Schloerke
aedbfb11af Use GitHub Actions (#2876) 2020-05-11 14:18:10 -04:00
Winston Chang
43fd380e25 Merge pull request #2881 from rstudio/joe/feature/better-delete-file 2020-05-11 13:09:51 -05:00
Joe Cheng
2872100ff0 Code review feedback 2020-05-11 11:06:51 -07:00
Alan Dipert
afe81048c9 Pass namespaced names to renderFile() 2020-05-11 17:51:35 +00:00
Alan Dipert
9d44857d77 get_mocked_publics() cleanup 2020-05-11 17:49:10 +00:00
Alan Dipert
4a7d186f27 renderCachedPlot() support, minor reorg and misc. fixes 2020-05-08 23:02:06 +00:00
Joe Cheng
5b1fd12edd Unit tests 2020-05-08 14:28:46 -07:00
Joe Cheng
8309a2aed9 Safer default file deletion behavior for renderImage 2020-05-08 14:18:57 -07:00
Alan Dipert
e15d6a2239 sp 2020-05-08 19:56:52 +00:00
Alan Dipert
abf04ac96f roxygenation 2020-05-08 19:50:04 +00:00
Carson Sievert
8ec6275f9a Merge pull request #2879 from rstudio/revert-renderedFamily
remove renderedFamily info field
2020-05-08 14:44:05 -05:00
Carson
3e2bfb20f5 remove renderedFamily info field
Because no information is better than wrong information
https://bugzilla.mozilla.org/show_bug.cgi?id=1252821
2020-05-08 13:36:05 -05:00
Winston Chang
ee13087d57 Merge pull request #2878 from rstudio/revert-ragg 2020-05-08 13:29:43 -05:00
Alan Dipert
f2fd7de9db roxygenation 2020-05-08 18:28:41 +00:00
Carson
dcfd7e05ce shiny.useragg should default to FALSE 2020-05-08 13:27:54 -05:00
Winston Chang
e47b69c33a Merge pull request #2874 from rstudio/wch-app-template-tweaks 2020-05-08 10:58:55 -05:00
Winston Chang
383f78c8ca Fix link 2020-05-08 10:58:19 -05:00
Alan Dipert
54042d5150 export MockShinySession; document 2020-05-07 23:02:00 +00:00
Alan Dipert
90e6ffc928 <<- to <- 2020-05-07 22:57:09 +00:00
Alan Dipert
be65f49bbd sapply => names() <- + lapply 2020-05-07 22:43:47 +00:00
Alan Dipert
d81b8ff98f ensyms => list 2020-05-07 22:42:20 +00:00
Alan Dipert
9b5201e33c Return invisible in noop functions 2020-05-07 22:38:01 +00:00
Alan Dipert
588c1b91b9 Test reactive() + downloadHandler 2020-05-07 22:30:45 +00:00
Alan Dipert
eb63734792 getCurrentOutputInfo() works, and tests 2020-05-07 21:36:42 +00:00
Alan Dipert
22cc585180 downloadHandler() test 2020-05-07 21:14:27 +00:00
Alan Dipert
bd1631a649 downloadHandler() test 2020-05-07 21:12:46 +00:00
Alan Dipert
e12bde6cdb getCurrentOutputInfo() 2020-05-07 20:23:38 +00:00
Alan Dipert
7763ceefc0 Update private$downloads docs 2020-05-07 18:55:45 +00:00
Alan Dipert
05a7d998b9 registerDownload() and private$renderFile 2020-05-07 18:53:07 +00:00
Carson Sievert
4b676ac327 Merge pull request #2873 from rstudio/output-info-docs
Document the return value of getCurrentOutputInfo()
2020-05-07 12:14:58 -05:00
Carson
6ad1322734 Output context isn't well-defined 2020-05-07 12:08:32 -05:00
Alan Dipert
03248735ac Un-noop a couple things we need to implement 2020-05-07 16:36:31 +00:00
Winston Chang
8e5651490c Merge pull request #2851 from rstudio/tabsetPanelBody_value_check 2020-05-07 11:17:21 -05:00
Barret Schloerke
0d7aa2a101 undo id parameter for example 2020-05-07 12:14:16 -04:00
Winston Chang
83669ced3d App template tweaks 2020-05-07 10:52:16 -05:00
Winston Chang
2a33d23165 Make runnable example for downloadButton 2020-05-07 10:51:24 -05:00
Alan Dipert
b7ae915784 Remove unnecessary constructor 2020-05-07 15:37:24 +00:00
Winston Chang
9fcfa25460 Merge pull request #2872 from rstudio/wch-sort-c 2020-05-07 10:26:28 -05:00
Carson
946eae00bd roxygen 2020-05-07 10:21:17 -05:00
Carson
e8ef33c9a1 Explain the return value of getCurrentOutputInfo() 2020-05-07 10:18:32 -05:00
Barret Schloerke
8c8654f2d8 feedback from @wch. Keep 'value' arg, drop 'id' 2020-05-07 10:55:12 -04:00
Winston Chang
4b744791f2 Add sort_c function for sorting in C locale 2020-05-07 09:47:02 -05:00
Barret Schloerke
5327cb33f9 Merge branch 'master' into tabsetPanelBody_value_check
* master:
  Remove trailing comma and extra return statement
  Remove check note about function not found
2020-05-07 10:44:02 -04:00
Barret Schloerke
d9ddc6fd90 Merge pull request #2871 from rstudio/schloerke-patch-2 2020-05-07 10:43:17 -04:00
Barret Schloerke
a94c2cfa1e Merge pull request #2870 from rstudio/schloerke-patch-1 2020-05-07 10:35:07 -04:00
Barret Schloerke
614bc6b480 Remove trailing comma and extra return statement 2020-05-07 10:34:09 -04:00
Barret Schloerke
47585174d8 Remove check note about function not found 2020-05-07 10:32:36 -04:00
Barret Schloerke
5dd11bcc9b Merge branch 'master' into tabsetPanelBody_value_check
* master: (63 commits)
  Use getStyle() to support old browsers
  safe-guard against NA values
  make shiny.useragg an unofficial option that takes priority over quartz
  smaller bump in version
  update news and add to comment
  Wrap styles in reactive() so that calling getCurrentOutputInfo() doesn't always invalidate
  Use is_available() more widely and remove unneeded complexity in check_suggested()
  comments
  generalize internal is_installed and use it in startPNG()
  Remove reference to helper-load.R that's no longer there
  Minor updates to shinyAppTemplate docs
  code review with Winston
  default to FALSE for now
  let showtext know about the resolution
  Pass check
  Rollback the custom device arg (may come later) in favor of a shiny.useragg option
  Auto values aren't resolved until plot time, so if we see one, resolve it
  change device bg default only if the thematic option is set
  somehow messed up rebase
  missed passing device in renderCachedPlot()
  ...
2020-05-07 10:22:59 -04:00
Alan Dipert
160d2123b2 roxygen tweaks 2020-05-07 05:42:53 +00:00
Alan Dipert
ca55ed3a21 Down to 1 missing method and 1 missing field 2020-05-06 22:58:07 +00:00
Alan Dipert
34fe820a26 Fix ls() usage, revert MockShinySession roxygen tags 2020-05-06 22:18:56 +00:00
Winston Chang
c7a4d23662 Merge pull request #2740 from rstudio/joe/feature/bs4-compat-plot-colors 2020-05-06 15:51:11 -05:00
Alan Dipert
432a7120f2 Clarify test logic; better output 2020-05-06 20:44:18 +00:00
Alan Dipert
7e8b5d28f7 fix tests 2020-05-06 20:18:24 +00:00
Carson
079871df38 Use getStyle() to support old browsers 2020-05-06 15:17:06 -05:00
Carson
c95d3ef07d safe-guard against NA values 2020-05-06 15:16:22 -05:00
Carson
acad455ccb make shiny.useragg an unofficial option that takes priority over quartz 2020-05-06 15:10:42 -05:00
Alan Dipert
fcc7df32ad Add instance methods in the constructor 2020-05-06 19:09:49 +00:00
Carson
c7f0484c37 smaller bump in version 2020-05-06 13:24:57 -05:00
Carson
3dac31a771 update news and add to comment 2020-05-06 13:21:17 -05:00
Alan Dipert
16357963d5 Add more noops and errors 2020-05-05 23:05:24 +00:00
Alan Dipert
7d7492b9aa roxygen flailing 2020-05-05 21:51:04 +00:00
Carson
26dff7e00e Wrap styles in reactive() so that calling getCurrentOutputInfo() doesn't always invalidate 2020-05-05 15:30:04 -05:00
Carson
cf410e310f Use is_available() more widely and remove unneeded complexity in check_suggested() 2020-05-05 13:30:01 -05:00
Carson
5d4855f86c comments 2020-05-05 12:26:58 -05:00
Carson
6d7e2b8a06 generalize internal is_installed and use it in startPNG() 2020-05-05 12:26:44 -05:00
Barret Schloerke
d37be0d059 Update tests/testthat/test-tabPanel.R
Co-authored-by: Hadley Wickham <h.wickham@gmail.com>
2020-05-05 12:39:02 -04:00
Barret Schloerke
d419ec5776 Update tests/testthat/test-tabPanel.R
Co-authored-by: Hadley Wickham <h.wickham@gmail.com>
2020-05-05 12:38:57 -04:00
Alan Dipert
c01f100858 WIP noop refactor party; collation brokn 2020-05-05 03:46:25 +00:00
Winston Chang
d8e380b53f Merge pull request #2860 from mine-cetinkaya-rundel/app-template-edits 2020-05-04 17:27:05 -05:00
Alan Dipert
6881c39c8d Use noop() 2020-05-04 20:19:00 +00:00
Mine Çetinkaya-Rundel
e7fa540403 Remove reference to helper-load.R that's no longer there 2020-05-02 11:20:44 +01:00
Mine Çetinkaya-Rundel
e92ba27893 Minor updates to shinyAppTemplate docs
More detail on how shinyAppTemplate choices + streamline shinytest/testthat listing
2020-05-02 11:05:50 +01:00
Carson
210792397d code review with Winston 2020-05-01 16:20:15 -05:00
Alan Dipert
91385967c1 A good test of real/mock session difference 2020-05-01 21:01:01 +00:00
Alan Dipert
0c8d27964b add mock method notify func 2020-05-01 20:01:24 +00:00
Alan Dipert
ed4fcb71f1 testServer(): Users can now pass in their own session 2020-05-01 15:03:50 +00:00
Carson
0738f6a2d6 default to FALSE for now 2020-04-30 18:14:20 -05:00
Carson
1e2a874067 let showtext know about the resolution 2020-04-30 17:50:19 -05:00
Carson
9d2f8cbd8a Pass check 2020-04-30 17:50:19 -05:00
Carson
dd1c653365 Rollback the custom device arg (may come later) in favor of a shiny.useragg option 2020-04-30 17:50:19 -05:00
Carson
1a0a53a26f Auto values aren't resolved until plot time, so if we see one, resolve it 2020-04-30 17:50:19 -05:00
Carson
97ea4e2a26 change device bg default only if the thematic option is set 2020-04-30 17:50:19 -05:00
Carson
b408d9348d somehow messed up rebase 2020-04-30 17:50:19 -05:00
Carson
44cfde7a0c missed passing device in renderCachedPlot() 2020-04-30 17:50:19 -05:00
Carson
db3d7ee436 bump version 2020-04-30 17:50:19 -05:00
Carson
a05f713e26 code review with Barret 2020-04-30 17:50:19 -05:00
Carson
dfb492493c Provide a device argument to renderPlot() and plotPNG() 2020-04-30 17:50:19 -05:00
Carson
7ddf4169b8 no need to call thematic 2020-04-30 17:50:19 -05:00
Carson
89d6a3d91a User-supplied bg to renderPlot should take 1st priority 2020-04-30 17:50:19 -05:00
Carson
16196eeaaa ragg will take priority over Cairo 2020-04-30 17:50:19 -05:00
Carson
393d4163c8 Auto-theming interface will come from thematic 2020-04-30 17:50:19 -05:00
Carson
5855aa2689 First pass at an auto-theming interface 2020-04-30 17:50:19 -05:00
Carson
4e59f55f11 wip font support 2020-04-30 17:50:19 -05:00
Carson
b269487a47 port auto-theming logic to new thematic package 2020-04-30 17:50:19 -05:00
Carson
dce4028786 sequential colorscale now mixes fg/bg with accent (for the endpoints) 2020-04-30 17:50:19 -05:00
Carson
819ad4c770 newpage should always come before ggplot_build 2020-04-30 17:50:19 -05:00
Carson
b10b6d4833 default to bg='white', not transparent 2020-04-30 17:50:19 -05:00
Carson
a3d224beaf suggest scales; other R CMD check things 2020-04-30 17:50:19 -05:00
Carson
164ad8c521 Add autoThemeOptions() to pkgdown 2020-04-30 17:50:19 -05:00
Carson
4c8ec8befe allow autoTheme options to be reactive 2020-04-30 17:50:19 -05:00
Carson
0692334a27 cleanup 2020-04-30 17:50:19 -05:00
Carson
be912cf2ce Set default scales via plot_env for old ggplot2 and options for new ggplot2 2020-04-30 17:50:19 -05:00
Carson
f942c088ec mix colors using scales::colour_ramp 2020-04-30 17:50:19 -05:00
Carson
330da2dcbb code review feedback; introduce autoThemeOptions() 2020-04-30 17:50:19 -05:00
Joe Cheng
afad0395ff Use htmltools::parseCssColors 2020-04-30 17:50:19 -05:00
Carson
ecd72f1bc0 Add sequential colorscale for ggplot2 based on the accent color
Also, some R CMD check fixes and other cleanup
2020-04-30 17:50:19 -05:00
Carson
867daeead7 Add accent (link) color and qualitative color palettes
Also, improved approach to adjusting various defaults based on fg/bg/accent
2020-04-30 17:50:19 -05:00
Carson
a20c3a397e make sure ggplot_apply_auto_colors always returns a built plot 2020-04-30 17:50:19 -05:00
Carson
a1a22e811f Report font-family and colors in getCurrentOutputInfo() if .shiny-report-theme class is present
Also, rename autocolors to autotheme as we'd like to support fonts and possibly more in the future

    Also, wrap ggplot2 default overriding and building logic into one function, so plotly can use it in a self-contained fashion
2020-04-30 17:50:19 -05:00
Joe Cheng
3fbbabd68a Better autocolors for bars and other geoms that don't have colour; lighter lattice defaults 2020-04-30 17:50:19 -05:00
Joe Cheng
47c1202535 Tweak ggplot2 colors to look closer to defaults 2020-04-30 17:50:19 -05:00
Joe Cheng
83a5feaaa8 More autocolor fixes
- Don't set graphical params if fg/bg are not set
- Make maybe_set_palette return the old palette
2020-04-30 17:50:19 -05:00
Joe Cheng
6e767fc71d More robust un-setting of autocolor params
Before this commit, any error would cause the params not to be restored.
If the error was in the initial parameter setting itself (as can occur
with lattice_set_params() at the moment), then not even the graphics
device would be restored, meaning that stopping the app and attempting
to plot would result in graphics being sent to a temp file somewhere
instead of the default graphics device.
2020-04-30 17:50:19 -05:00
Joe Cheng
303f264326 Implement plot.autocolors for cached plots 2020-04-30 17:50:19 -05:00
Carson
f73671845c Add support for lattice and better default palette() for col scaling 2020-04-30 17:50:19 -05:00
Carson
82c04caf3a facet strips shouldn't have stroke and fill should be a semi-lighten fg color 2020-04-30 17:50:19 -05:00
Carson
d8080d1336 only assign default for aes that are relevant and non-transparent 2020-04-30 17:50:19 -05:00
Carson
fc09d1c09a Support a named vector as an override to computed colors 2020-04-30 17:50:19 -05:00
Carson
65a47c01ec grep for hex codes properly and return exit length 0 character string if input is length 0 2020-04-30 17:50:19 -05:00
Carson
e9f2e0d7d7 wip 2020-04-30 17:50:19 -05:00
Winston Chang
ddcb31897d Merge pull request #2858 from rstudio/currentOutputInfo 2020-04-30 17:05:13 -05:00
Carson
eecdc0e24c update news 2020-04-30 14:56:14 -05:00
Carson
f642bcc954 Have getCurrentOutputInfo() return NULL instead of error if called in a non-reactive context (extension of #2707) 2020-04-30 14:54:09 -05:00
Carson Sievert
7dedac5880 Merge pull request #2857 from rstudio/jquery-3.5
Bump to jQuery 3.5.0
2020-04-30 14:47:22 -05:00
Carson
ee5362f81a news 2020-04-30 14:37:10 -05:00
Carson
1475137d4d yarn build 2020-04-30 14:27:10 -05:00
Carson
8ba028ebbb Use native String.trim() method since $.trim() is now deprecated 2020-04-30 10:35:14 -05:00
Carson
7cd385e8c2 Bump jquery 3.x to 3.5.0, closes #2856 2020-04-30 10:06:21 -05:00
Alan Dipert
41694b3666 testServer(): Properly capture module return values 2020-04-29 22:13:00 +00:00
Winston Chang
25314f370e Merge pull request #2852 from rstudio/remove_test 2020-04-28 11:14:06 -05:00
Barret Schloerke
664b88c1bc remove duplicate param docs 2020-04-28 11:06:45 -04:00
Barret Schloerke
d6adffa273 testServer does not return results. Do not test for it. 2020-04-28 10:58:44 -04:00
Barret Schloerke
5bd039a335 use 'id' instead of 'value' for tabPanel/tabPanelBody 2020-04-28 10:55:53 -04:00
Barret Schloerke
0782cc3c21 code feedback for tabPanelBody 2020-04-27 16:57:26 -04:00
Barret Schloerke
c73628cca1 Move 'value' to first arg position for tabPanelBody. 'value' is now required. Add tests 2020-04-27 16:10:36 -04:00
Winston Chang
8ffc5aa20c Merge pull request #2849 from daattali/patch-1 2020-04-27 13:50:31 -05:00
Winston Chang
89c2f09864 Clearer wording for dryrun option 2020-04-27 13:21:02 -05:00
Dean Attali
ee3115653c typo in NEWS 2020-04-25 01:15:25 -04:00
Winston Chang
48115fc150 Merge pull request #2842 from rstudio/missing_monitorHandle 2020-04-24 15:34:01 -05:00
Winston Chang
d804a363ae Merge pull request #2837 from rstudio/testServer_args 2020-04-24 15:33:49 -05:00
Barret Schloerke
867c084990 check if function, not if not null 2020-04-24 16:30:21 -04:00
Barret Schloerke
8ffbfca97b do not call monitorHandle unless it is set 2020-04-24 15:51:23 -04:00
Barret Schloerke
ca9a72d25c testServer should return invisible() 2020-04-24 10:06:35 -04:00
Barret Schloerke
acdbe8ef5e use list instead of rlang::list2 2020-04-23 17:47:52 -04:00
Alan Dipert
5cc3a5b71c Dynamic dots for MockShinySession$setInputs() (#2838)
* MockShinySession: add $click()

* Fix return value of MockShinySession$click()

* session$click() test w/ observeEvent

* session$click() test w/ observeEvent

* $click() examples

* $click, $setInputs: add \\dontrun

* $setInputs(): make dots dynamic

* document

* rm $click()
2020-04-23 16:36:27 -05:00
Winston Chang
bd587fd21b Fix pkgdown.yml 2020-04-23 16:36:01 -05:00
Barret Schloerke
0f580ff23d remove '../' from loadSupport calls as they will be found automatically now 2020-04-23 14:55:21 -04:00
Winston Chang
b0b105babc Merge pull request #2836 from hadley/interactive-helper-docs
Combine docs for nearPoints() and brushedPoints()
2020-04-23 13:46:03 -05:00
Hadley Wickham
3b0cc5f3a8 Rebuild docs 2020-04-23 13:37:26 -05:00
Barret Schloerke
e50981ccc0 replace ... with args in testServer 2020-04-23 14:19:03 -04:00
Winston Chang
24f3c20f26 Merge pull request #2814 from rstudio/hidden_tabset
Add `type = "hidden"` to `tabsetPanel`
2020-04-23 12:46:15 -05:00
Hadley Wickham
ca5d71a491 Combine docs for nearPoints() and brushedPoints()
* Mouse -> pointer
* Simplify panelvar docs
* Add new ggplot2 and brushing sections
2020-04-23 08:49:24 -05:00
Winston Chang
a022a2b4a4 Merge pull request #2766 from rstudio/joe/feature/autoreload-error
Support shiny.autoreload even when there are errors
2020-04-22 16:10:38 -05:00
Winston Chang
0cb618b9b1 Merge pull request #2834 from hadley/output-args
Remove deprecated arguments from plotOutput/imageOuput
2020-04-22 09:39:22 -05:00
Winston Chang
1f4927683e Merge pull request #2829 from rstudio/wch-migrate-shinytest
Add migrateLegacyShinytest function
2020-04-22 09:21:46 -05:00
Winston Chang
7c74399a5d Documentation edits 2020-04-22 09:18:06 -05:00
Winston Chang
52903b6ecd Do not flush when setting a returned value for a mock shiny ses… (#2832)
Do not flush when setting a returned value for a mock shiny session
2020-04-22 09:09:42 -05:00
Alan Dipert
a43244916b loadSupport(): fix global.R support, run global.R in appropriat… (#2831)
* loadSupport(): fix global.R support, run global.R in appropriate dir

* loadSupport(): Use withr::with_dir, fix global.R-related tests

* shiny.autoload.r: Ensure dir set to appDir before sourcing R/ files

* Use file.path.ci() to ensure case-insensitive filesystem compat in loadSupport() and findEnclosingApp()

* loadSupport(): Ensure proper source order of R/ files

* loadSupport(): Clarify test
2020-04-22 08:54:06 -05:00
Hadley Wickham
35be892e69 Remove deprecated arguments from plotOutput/imageOuput
These were deprecated in 0.11.1, which was released on 2015-02-11, i.e. >5 years ago.
2020-04-22 08:45:40 -05:00
Barret Schloerke
536e8ffb28 Do not set a returned value for an app
An app never has access to the returned value of a server function.  This DOES makes sense for modules, but not shiny apps.
2020-04-21 16:57:56 -04:00
Barret Schloerke
0241f07105 Do not flush when setting the returned value in a mocked shiny session
This requires $flushReact() to be called when wanting to access reactive values that do not require inputs to be set
2020-04-21 16:57:15 -04:00
Winston Chang
3570af90ab Update test for new function name 2020-04-17 17:32:45 -05:00
Winston Chang
fa3fa9e2ef Add migrateLegacyShinytest function 2020-04-17 17:28:03 -05:00
Winston Chang
83e2bb028f Small fixes 2020-04-17 17:27:37 -05:00
Alan Dipert
f50b7c4301 testServer() and loadSupport(): if app is a path, and not an ap… (#2823)
* Improve makeMask comment

* Added skeleton function and example

* Refinements to app template

* Template update

* Rename tests/shinytests/ to tests/shinytest/

* App template updates

* mask creation: clean up, document, and align with rlang::new_data_mask()

* Revert minor in mock session

* Document/fix mock session $setEnv() and $setReturned() behavior

* document

* simplify buildMask()

* minor

* simplify buildMask()

* simplify buildMask()

* add 12_counter test app to exercise runTests + testServer

* Add appobj test

* WIP loadSuppor for apps passed to testServer

* Revert "WIP loadSuppor for apps passed to testServer"

This reverts commit 2d519aca15.

* Found and fixed app obj lifecycle methods that testServer was not exercising when applicable

* Rename 12_counter to 12_template

* Rename utils.R to sort.R

* Updates from code review

* Move 12_template to app_template dir

* Add informative comments

* Simplify mask building, default app to "." in testServer()

* testServer(): Error when arguments provided to a server function

* Fix tests; don't default autoload to FALSE if not found

* Use withr::with_options in one particularly confusing shiny.autoload.r-related test

* testServer(): if app is a path, and not an app, walk up dirs until an app is found

* Fix tests on Windows - rprojroot uses winslash='/'

* testServer(): raise findEnclosingApp() call

* Add library(shiny) to top of test app

* document

* Use require(shiny) in testServer() it works without library(shiny)

* Revert "testServer(): raise findEnclosingApp() call"

This reverts commit 5801dee2a4.

* document

* loadSupport(): appDir now defaults to . and findEnclosingApp() occurs

* loadSupport() and testServer(): default app/appDir to NULL

* Remove sketchy test involving detach()

* Move findEnclosingApp() to utils.R

* Dropped rprojroot dep and moved findEnclosingApp() to utils

* Better error message

* findEnclosingApp(): Fix case when root is an app

Co-authored-by: trestletech <jeff.allen@trestletechnology.net>
Co-authored-by: Winston Chang <winston@stdout.org>
2020-04-17 16:04:40 -05:00
Winston Chang
41c9a0c395 shinyAppTemplate tweaks (#2828)
* shinyAppTemplate: Add dryrun option and print out changes

* Code cleanup

* Add shinytest version check

* Move is_template logic into function

* Use dirExists function

* Use version check compatible with dev version

* Small fixes

* More refactoring

* Fix message about shinytest

* Documentation formatting fixes
2020-04-17 15:53:51 -05:00
Barret Schloerke
12401b6588 Merge pull request #2826 from rstudio/barret_runTests2
Update runTests() add print method
2020-04-17 13:50:21 -04:00
Barret Schloerke
8edf8905a5 Merge pull request #2827 from rstudio/drop_serverR
Drop server.R template file
2020-04-17 13:37:26 -04:00
Barret Schloerke
d5cb8d187c code feedback 2020-04-17 13:24:00 -04:00
Barret Schloerke
328a066f0f merge news items 2020-04-17 11:08:37 -04:00
Barret Schloerke
42d314d592 safeguard testing by checking for some suggested packages 2020-04-17 11:02:19 -04:00
Barret Schloerke
d89d546e53 make sure shinytest is installed from github. Needs latest version 2020-04-17 10:40:20 -04:00
Barret Schloerke
1a558143c7 add comments 2020-04-17 10:39:45 -04:00
Barret Schloerke
ad7ffa2245 use mkdir_p to always create directories 2020-04-17 10:09:16 -04:00
Barret Schloerke
717ac420d9 fix test 2020-04-17 09:59:34 -04:00
Barret Schloerke
abff323eb6 display the test folder name when it fails 2020-04-17 09:50:54 -04:00
Barret Schloerke
03bc1ccd4a remove autoload.r test, as that code path doesn't exist anymore 2020-04-17 09:50:47 -04:00
Barret Schloerke
da408eeaff removed dplyr 2020-04-17 09:50:45 -04:00
Barret Schloerke
a2ba9bb26a Test module app 107_scatterplot. Use pretty paths when printing the runTests output 2020-04-17 09:50:42 -04:00
Barret Schloerke
16c41ed046 Document 2020-04-17 09:50:39 -04:00
Barret Schloerke
aeb3c9f094 Test many combinations of shinyAppTempalte combos. Do not full matrix as shinytest is slow to execute 2020-04-17 09:50:37 -04:00
Barret Schloerke
2562cc8220 shiny.autoload.r is not required for runTests anymore 2020-04-17 09:50:29 -04:00
Barret Schloerke
0647cd85e9 If no module is used with shinytest, do not test the module 2020-04-17 09:49:52 -04:00
Barret Schloerke
d57e7389d2 feedback - remove all non-module/server test files in the testthat dir if the R folder is not used 2020-04-17 09:49:50 -04:00
Barret Schloerke
3cb3316a95 Copy all files, but if they are glue files... use the template 2020-04-17 09:49:47 -04:00
Barret Schloerke
8ba03e1205 Have output$sequence test be conditional on if the r dir is used 2020-04-17 09:49:45 -04:00
Barret Schloerke
6a69d3c07b feedback - no expr = 2020-04-17 09:49:42 -04:00
Barret Schloerke
c054b8c9ab feedback - drop adhoc 2020-04-17 09:49:39 -04:00
Barret Schloerke
db6f7cceea feedback - Do not require req(input$size) 2020-04-17 09:49:37 -04:00
Barret Schloerke
0898ee1fba Remove runTests output 2020-04-17 09:49:27 -04:00
Barret Schloerke
6366c0a684 Add full template + runTests test 2020-04-17 09:48:01 -04:00
Barret Schloerke
f56eb42c90 use adhoc.R in stead of server.R in shinyAppTemplate 2020-04-17 09:48:01 -04:00
Barret Schloerke
6f3f21921e No longer need helper-support. testthat.R should do this before running 2020-04-17 09:47:56 -04:00
Barret Schloerke
b8c016c3e9 print the app folder name (if available), not just the test file name 2020-04-16 19:13:51 -04:00
Barret Schloerke
e5d3b1c1d5 Code feedback. snake case legacy_shinytest fn. add comments / change error 2020-04-16 19:13:51 -04:00
Barret Schloerke
fe140b6319 Update tests with the design that shinytest legacy can not be called. Add assert=FALSE where appropriate 2020-04-16 19:13:48 -04:00
Barret Schloerke
4e1e0aad8a Update to use withr / loadSupport 2020-04-16 19:13:03 -04:00
Barret Schloerke
84a5515a3d Throw error on legacy shinytest testing instead of allowing the legacy test structure 2020-04-16 19:13:03 -04:00
Barret Schloerke
0d5073f8ff Commit revert 2020-04-16 19:13:03 -04:00
Barret Schloerke
05a4a101db Update app to be consistent. Add testthat contexts 2020-04-16 19:13:03 -04:00
Barret Schloerke
848f18be2b Add contexts and pass all tests 2020-04-16 19:13:03 -04:00
Barret Schloerke
21c9079087 Update failure name 2020-04-16 19:13:03 -04:00
Barret Schloerke
2935192eec Enable broken adhoc test 2020-04-16 19:13:03 -04:00
Barret Schloerke
f896db033f Rename output 2020-04-16 19:13:03 -04:00
Barret Schloerke
b197afe1a0 Edit docs 2020-04-16 19:13:03 -04:00
Barret Schloerke
dd07f7f580 Document 2020-04-16 19:13:03 -04:00
Barret Schloerke
8376f9093b white space and small comments 2020-04-16 19:13:03 -04:00
Barret Schloerke
38b8ed7bf9 Add an environment argument to runTests 2020-04-16 19:13:03 -04:00
Barret Schloerke
aa74ea0d0a Remove code specifically looking for shinytest only files. This is not necessary as it will still work in the current setup. 2020-04-16 19:13:03 -04:00
Barret Schloerke
e5d3f62043 add another testing app that has a module that returns a reactive value 2020-04-16 19:13:03 -04:00
Barret Schloerke
d2d0e70678 Each testing environment must require their own loadSupport call if necessary 2020-04-16 19:13:03 -04:00
Barret Schloerke
aceb7d0467 Add assert logic 2020-04-16 19:13:03 -04:00
Barret Schloerke
c7ac1fa630 add print method 2020-04-16 19:13:03 -04:00
Barret Schloerke
5855a5b26c Reprint error 2020-04-16 19:13:03 -04:00
Barret Schloerke
0301af62b8 Add todo 2020-04-16 19:13:03 -04:00
Barret Schloerke
32e9757bf7 pass tests 2020-04-16 19:12:59 -04:00
Barret Schloerke
d2b883c4b5 Merge error / result column as pass can be used to determine what the value is 2020-04-16 19:06:45 -04:00
Alan Dipert
816f40a2d5 Consolidate testServer() fixes and enhancements (#2815)
* Improve makeMask comment

* Added skeleton function and example

* Refinements to app template

* Template update

* Rename tests/shinytests/ to tests/shinytest/

* App template updates

* mask creation: clean up, document, and align with rlang::new_data_mask()

* Revert minor in mock session

* Document/fix mock session $setEnv() and $setReturned() behavior

* document

* simplify buildMask()

* minor

* simplify buildMask()

* simplify buildMask()

* add 12_counter test app to exercise runTests + testServer

* Add appobj test

* WIP loadSuppor for apps passed to testServer

* Revert "WIP loadSuppor for apps passed to testServer"

This reverts commit 2d519aca15.

* Found and fixed app obj lifecycle methods that testServer was not exercising when applicable

* Rename 12_counter to 12_template

* Rename utils.R to sort.R

* Updates from code review

* Move 12_template to app_template dir

* Add informative comments

* Simplify mask building, default app to "." in testServer()

* testServer(): Error when arguments provided to a server function

* Fix tests; don't default autoload to FALSE if not found

* Use withr::with_options in one particularly confusing shiny.autoload.r-related test

Co-authored-by: trestletech <jeff.allen@trestletechnology.net>
Co-authored-by: Winston Chang <winston@stdout.org>
2020-04-16 10:26:55 -05:00
Carson Sievert
7e7f38005a Merge pull request #2820 from rstudio/with-path
New path tag causes scoping issue in showcase mode
2020-04-14 13:47:13 -05:00
Barret Schloerke
fb834f7207 roxygen sorted the reexports 2020-04-14 14:30:42 -04:00
Winston Chang
5a3e5296d0 Fix typo 2020-04-14 12:51:20 -05:00
Winston Chang
a0e8d8f2d8 Update NEWS 2020-04-14 11:25:42 -05:00
Carson
9c6dfff531 document 2020-04-14 10:55:05 -05:00
Carson
84d9580bae New path tag causes scoping issue in showcase mode 2020-04-14 10:42:10 -05:00
Jeff Allen
8d6de642ea [WIP] Add skeleton function and example (#2704)
* Added skeleton function and example

* Refinements to app template

* Template update

* Rename tests/shinytests/ to tests/shinytest/

* App template updates

* Rename 12_counter to 12_template

* Rename utils.R to sort.R

* Updates from code review

* Move 12_template to app_template dir

* Add informative comments

* Add shinyAppTemplate to pkgdown.yml

* Fixes for LaTeX docs

Co-authored-by: Winston Chang <winston@stdout.org>
2020-04-14 09:45:10 -05:00
Winston Chang
b20b812cfe Merge pull request #2819 from hadley/opts-id
Don't set default id in clickOpts() and friends
2020-04-14 09:04:50 -05:00
Hadley Wickham
9b23ff6a19 Don't set default id in clickOpts() and friends
This was especially confusing given that each function tests that the id is not NULL.
2020-04-14 08:31:27 -05:00
Winston Chang
cc5278a117 Don't print loading R/ dir messages (#2817)
* Don't print loading R/ dir messages

* Remove obsolete tests
2020-04-13 18:10:32 -05:00
Barret Schloerke
ca6459afe4 add !important attr to .nav-hidden css class 2020-04-09 15:29:01 -04:00
Barret Schloerke
f8477f007d use a list. Use @cpsievert 's wording suggestion. 2020-04-09 15:26:08 -04:00
Barret Schloerke
82d1ad278c merge master 2020-04-09 12:43:55 -04:00
Barret Schloerke
761fb608d3 Add updateActionLink (#2811)
* Add updateActionLink function and example

* document

* add news item
2020-04-09 12:27:45 -04:00
Barret Schloerke
af328eee90 add news items. Add tabPanelBody() function. Document 2020-04-09 11:47:25 -04:00
Barret Schloerke
0fde11ae72 document 2020-04-09 10:44:07 -04:00
Barret Schloerke
73919b1943 add type = 'hidden' for tabsetPanel to hide the tab headers 2020-04-09 10:43:47 -04:00
Winston Chang
1433439215 Merge pull request #2737 from rstudio/inline-markdown
Add shiny::markdown() for inline Markdown
2020-04-08 11:28:27 -05:00
Alan Dipert
4c8dc09f67 NEWS.md 2020-04-08 16:15:22 +00:00
Alan Dipert
80b43942b0 Bump glue dep 2020-04-08 16:14:01 +00:00
Winston Chang
b709b53b6a Merge pull request #2807 from rstudio/serverModule-testModule
moduleServer/testServer overhaul
2020-04-08 10:58:03 -05:00
Alan Dipert
f4e3e5b618 server => module 2020-04-08 05:06:30 +00:00
Alan Dipert
bac7299359 Remove strings from expect_error 2020-04-08 04:57:14 +00:00
Alan Dipert
fc6f535edd Clarify testServer lexenv assertions 2020-04-08 04:54:32 +00:00
Alan Dipert
7e2ffab62c Use base versions of a couple rlang::env_* functions 2020-04-08 04:44:25 +00:00
Alan Dipert
214d721380 Move session$env sanity check out of makeMask and into testServer 2020-04-08 04:41:51 +00:00
Alan Dipert
2f8227e652 Un-inline assignment 2020-04-08 04:36:00 +00:00
Alan Dipert
c0c02d290f Remove unused variable 2020-04-08 04:35:40 +00:00
Alan Dipert
bc2aa71888 Use vapply in mapNames() 2020-04-08 04:27:40 +00:00
Alan Dipert
7f187d1553 Add markdown() NEWS item 2020-04-08 03:04:46 +00:00
Alan Dipert
81b1f4fdc1 Inline markdown tests, add to docs 2020-04-07 22:48:04 +00:00
Alan Dipert
15f088f10a Merge remote-tracking branch 'origin/master' into inline-markdown 2020-04-07 20:47:11 +00:00
Alan Dipert
286f12522b document 2020-04-07 18:40:40 +00:00
Alan Dipert
9d8a6d0142 Document new R6 methods 2020-04-07 18:40:34 +00:00
Alan Dipert
a2dd97cc74 Merge remote-tracking branch 'origin/master' into serverModule-testModule 2020-04-06 23:13:33 +00:00
Alan Dipert
1d9a6ea3c0 getEnv() => env, docs 2020-04-06 23:11:51 +00:00
Alan Dipert
3ca8b1017b Tests pass \o/ 2020-04-06 22:36:09 +00:00
Winston Chang
ecd7c76aee Merge pull request #2764 from rstudio/runTests-aggregate
Tweak runTests() output format
2020-04-06 15:59:06 -05:00
Alan Dipert
70edcd62b9 Getting there 2020-04-03 23:04:50 +00:00
Alan Dipert
90f531888c fix one failing test 2020-04-03 22:06:14 +00:00
Alan Dipert
953de733e7 nested module tests pass now, many others fail %-) 2020-04-02 23:26:02 +00:00
Alan Dipert
e0ed443319 WIP mock session scoped proxy 2020-04-02 21:33:57 +00:00
Alan Dipert
1487720fd8 WIP more markdown() tests 2020-04-01 21:38:39 +00:00
Alan Dipert
828567e0ce Add failing proxy-related and ns() related tests 2020-04-01 21:31:39 +00:00
Alan Dipert
78da4c7fce Merge remote-tracking branch 'origin/master' into serverModule-testModule 2020-04-01 07:26:43 +00:00
Alan Dipert
7f80bfd2cb document 2020-04-01 07:01:57 +00:00
Alan Dipert
7e3deb5e3f document 2020-04-01 06:55:44 +00:00
Alan Dipert
5475ec4f0c document 2020-04-01 06:48:34 +00:00
Alan Dipert
58b4585b57 Doc and test updates 2020-04-01 06:45:18 +00:00
Alan Dipert
cf9ab1c47b appobj coercion works 2020-03-31 21:36:20 +00:00
Alan Dipert
65233cdd5c First passing app dir test for testServer overhaul 2020-03-31 06:33:01 +00:00
Alan Dipert
9d13cb644d test-module.R => test-server.R 2020-03-31 05:36:03 +00:00
Alan Dipert
dd9e0343e8 More test progress 2020-03-31 05:29:42 +00:00
Alan Dipert
bb4aaa2a78 Bring back scope tests 2020-03-31 05:02:31 +00:00
Alan Dipert
0023418b94 More test reorg 2020-03-30 22:55:27 +00:00
Alan Dipert
ec2c9ecea0 Split up and rename various tests 2020-03-27 22:43:29 +00:00
Barret Schloerke
59759398a6 Update actionButton example (#2806)
* Update actionButton example

* Fix link name in example
2020-03-27 17:00:56 -04:00
Alan Dipert
c4852cb451 Desired environment semantics are working 2020-03-25 22:40:08 +00:00
Winston Chang
99880d6e8a Merge pull request #2796 from rstudio/actionButton_class
Add css class examples for actionButton and actionLink
2020-03-24 09:52:23 -05:00
Alan Dipert
b005799d92 Add back many working/converted tests 2020-03-23 23:01:53 +00:00
Barret Schloerke
72f86dac27 Update Node.js build scripts (#2800)
* automatic formatting

* Update to the latest & greatest

* `./tools` `yarn build`

* Remove `newer` from grunt as it causes more problems than it solves. yarn build
2020-03-20 15:59:10 -04:00
Barret Schloerke
83628facb3 Use roxygen2 reexport setup for htmltools and fastmap (#2795)
* Remove all hard copied man files

* Move fastmap reexports into ./R/rexport-fastmap.R

* Update htmltools man script to look for the latest tag and make a reexports file

* document

* Update htmltools reexports to listen to ./inst/_htmltools_reexports.json file. Fix pkgdown failure.

* Allow for any package reexports. Currently fastmap and htmltools

* remove progress dependency

* add back skip on cran for pkgdown

* Test pkgdown only if called using devtools::test or within CI

* Removed outdated import docs file. Moved pkgdown and reexports files into ./tools/documentation. Updated travis file.

* add local test for pkgdown

* updated comments and added a debug comment

* print the repo url and close it

* print the downloaded repo tag info

* use gh package and make sure it's installed

* add PAT to travis and remove username from gh::gh call

* skip pkgdown test on cran and add comments
2020-03-20 14:25:18 -04:00
Alan Dipert
f6e171823a Merge remote-tracking branch 'origin/master' into serverModule-testModule 2020-03-19 20:48:07 +00:00
Barret Schloerke
9b743a319f Add ./revdep folder output and script (#2790)
* Revdep check. 719 - CRAN, 117 - BioConductor; 0 new problems; 3 failed to check

* fix last few install failures.  836 pass. 0 untested. 0 failures.

* add revdep script
2020-03-19 11:43:37 -05:00
Barret Schloerke
eedf2a6cc8 add examples for actionButton and actionLink which adds extra css classes 2020-03-17 14:21:20 -04:00
Winston Chang
e1e738f772 Change indenting of module examples 2020-03-17 13:01:50 -05:00
Winston Chang
182ff3df88 Merge tag 'v1.4.0.2'
Shiny 1.4.0.2 on CRAN
2020-03-16 12:34:10 -05:00
Barret Schloerke
23fde95f9e Add class 'function' to reactive and reactiveVal objects (#2793)
* add the class 'function' to a reactive object

* add the class 'function' to a reactiveVal object

* add test to make sure the reactive and reactiveVal objects are functions

* Add news item
2020-03-16 10:42:28 -04:00
Barret Schloerke
78f9132eb3 Fix Travis checks (#2791)
* make sure the server is only run if the example is interactive

* get travis to pass for now

* Update modules.R

* Add S3 class to MockShinySession

* un-skip test

* Update roxygen level

Co-authored-by: Alan Dipert <alan@dipert.org>
2020-03-12 17:42:38 -04:00
Winston Chang
84b7211588 Bump version to 1.4.0.2 2020-03-12 15:42:26 -05:00
Winston Chang
2793e15c26 Add link 2020-03-12 15:41:21 -05:00
Winston Chang
36bd76607a Skip debounce/throttle tests on CRAN 2020-03-12 15:41:16 -05:00
Barret Schloerke
e17f416bb0 Update roxygen level 2020-03-12 16:30:18 -04:00
Barret Schloerke
a577b1e22e un-skip test 2020-03-12 16:20:13 -04:00
Alan Dipert
2d324c77c1 Add S3 class to MockShinySession 2020-03-12 13:18:23 -07:00
Alan Dipert
88374eca74 Update modules.R 2020-03-12 13:16:55 -07:00
Barret Schloerke
386135788b get travis to pass for now 2020-03-12 16:12:05 -04:00
Barret Schloerke
a943d955dd make sure the server is only run if the example is interactive 2020-03-12 15:58:46 -04:00
Winston Chang
15476ac32e Merge pull request #2789 from rstudio/skip-tests-timing
Skip debounce/throttle tests on CRAN
2020-03-12 11:16:20 -05:00
Winston Chang
17fb5b9eae Add link 2020-03-12 10:00:36 -05:00
Winston Chang
fd27a0dfa2 Skip debounce/throttle tests on CRAN 2020-03-12 09:46:18 -05:00
Winston Chang
5ffe69ec6c Merge tag 'v1.4.0.1' 2020-03-12 09:21:07 -05:00
Carson
f5723b2a4d revert man/ changes to reflect CRAN version of htmltools 2020-03-11 12:04:58 -05:00
Alan Dipert
9e959a88f1 tests 2020-03-05 18:02:26 +00:00
Alan Dipert
09abac41c5 minor reformat 2020-03-05 17:03:15 +00:00
Alan Dipert
1dbf013c1b markdown(): Improve docs; add .noWS; class result as html 2020-03-05 16:57:43 +00:00
Alan Dipert
a637d5b126 Merge remote-tracking branch 'origin/master' into inline-markdown 2020-03-04 21:25:57 +00:00
Alan Dipert
d409183751 Merge remote-tracking branch 'origin/master' into runTests-aggregate 2020-03-04 21:03:50 +00:00
Alan Dipert
e8feef1ce0 Address feedback 2020-03-04 21:03:04 +00:00
Winston Chang
01491cc696 Merge pull request #2772 from rstudio/test-nest-testModule-testServer
Small changes to testModule()/testServer() semantics
2020-03-04 14:58:30 -06:00
Alan Dipert
568a3f28cf Change test to not be locale-dependent 2020-03-04 20:48:21 +00:00
Winston Chang
02219df480 Merge pull request #2773 from rstudio/wch-module
Add moduleServer function
2020-03-04 09:24:58 -06:00
Winston Chang
e006ca51ee Add NEWS item 2020-03-04 09:23:27 -06:00
Winston Chang
86f651f3ec Add moduleServer function 2020-03-04 09:23:27 -06:00
Carson
212b33a0ce bump version 2020-03-03 18:50:41 -06:00
Carson
6b7a121161 yarn build 2020-03-03 18:50:34 -06:00
Carson Sievert
c89da718b1 Merge pull request #2777 from rstudio/fix-docs
Run tools/updateHtmltools.R
2020-03-03 18:40:27 -06:00
Carson
eef3ae8387 update news and update htmltools docs 2020-03-03 16:45:57 -06:00
Alan Dipert
0975a61725 Add test to mitigate shadow with unquote 2020-03-03 22:32:59 +00:00
Winston Chang
0c53d54347 Merge pull request #2776 from rstudio/grid-r-devel
Patches for grid 4.0
2020-03-03 16:12:39 -06:00
Carson
cbbb04cf69 yarn build 2020-03-03 15:52:25 -06:00
Carson
120baf0a6e review feedback 2020-03-03 15:34:32 -06:00
Carson
685dc7cc3a Updates for new grid in r-devel 2020-03-03 15:30:12 -06:00
Alan Dipert
2fbb2ac77b Merge remote-tracking branch 'origin/master' into test-nest-testModule-testServer 2020-03-03 19:25:49 +00:00
Alan Dipert
2832db7aba New session$env test 2020-03-03 19:25:14 +00:00
Alan Dipert
18f2471d7c Fix some roxygen errors 2020-03-03 19:08:49 +00:00
Alan Dipert
ea28f5a61b Minor changes and tests 2020-03-03 18:58:42 +00:00
Winston Chang
fe9cc6038e Merge pull request #2774 from rstudio/no-slack-notifications
Remove slack notifications
2020-03-03 12:09:39 -06:00
Barret Schloerke
5ed335c499 Remove slack notifications 2020-03-03 12:42:48 -05:00
Alan Dipert
fd04b97496 Fix global reference test inside testServer 2020-03-02 17:56:47 +00:00
Alan Dipert
4c9d281b59 Subtle change to .testModule() semantics 2020-02-28 22:49:37 +00:00
Joe Cheng
a26d66b424 Respect shiny.autoreload option being set in app.R or global.R 2020-02-25 20:11:54 -08:00
Alan Dipert
cfb683419f Remove Rd 2020-02-25 20:30:34 +00:00
Alan Dipert
97887bdf02 Add noRd to private function 2020-02-25 20:30:07 +00:00
Alan Dipert
38ea693e73 NEWS.md updates 2020-02-25 18:13:49 +00:00
Alan Dipert
582a0ea6a5 Remove redundant new test 2020-02-25 17:21:19 +00:00
Alan Dipert
71b9f0907e Merge remote-tracking branch 'origin/master' into runTests-aggregate 2020-02-25 16:39:54 +00:00
Alan Dipert
82b82b714d Fix some roxygen errors 2020-02-25 16:27:01 +00:00
Alan Dipert
6356228053 Normalize runTests() output, improve documentation of returned dataframe 2020-02-25 00:08:51 +00:00
Alan Dipert
18fd677550 Pass existing runTests() tests 2020-02-24 22:48:08 +00:00
Alan Dipert
d9698df721 More progress on runTests() format 2020-02-24 22:14:24 +00:00
Joe Cheng
63839fe045 Support shiny.autoreload even when there are errors 2020-02-22 12:22:59 -08:00
Joe Cheng
2ee06a7cbf Revert "Support shiny.autoreload even when there are errors"
This reverts commit cf2ba90b1d.
2020-02-22 12:21:58 -08:00
Joe Cheng
cf2ba90b1d Support shiny.autoreload even when there are errors 2020-02-22 12:20:12 -08:00
Alan Dipert
8124b2143b revert names in app1 tests 2020-02-21 23:57:52 +00:00
Alan Dipert
5361573051 Add working runTests test 2020-02-21 23:35:19 +00:00
Alan Dipert
1d377c868d runTests() output 2020-02-21 22:17:34 +00:00
Alan Dipert
b0a855a326 Make improvements suggested by @schloerke 2020-02-19 20:39:28 +00:00
Alan Dipert
fa35f29596 add_result() for test run 2020-02-19 06:55:32 +00:00
Alan Dipert
f429d23b6e add_result() for test run 2020-02-19 06:54:46 +00:00
Alan Dipert
eeeb903b70 add_result() for test run 2020-02-19 06:54:11 +00:00
Alan Dipert
78f12c4a75 Type stability changes to shinytestrun object 2020-02-19 06:50:58 +00:00
Barret Schloerke
c69f34d1e2 update js files (version bump) 2020-02-18 13:47:02 -05:00
Barret Schloerke
ccfcc5d8b4 add news item 2020-02-18 13:47:02 -05:00
Barret Schloerke
210c248264 bump version 2020-02-18 13:47:02 -05:00
Barret Schloerke
e3258657d0 Invoke onSessionEnded callbacks with self reactive domain 2020-02-18 13:47:02 -05:00
Barret Schloerke
dbc518bf53 Fix broken timer tests and check htmltools docs (#2758)
* Adjust time so that it's in seconds and use expect_true to use regular R dispatch

* Execute './tools/updateHtmltoolsMan.R'

* add check for htmltools docs being up to date
2020-02-14 11:36:51 -05:00
Winston Chang
cdbdb4510e Fix NEWS item 2020-01-16 11:43:51 -06:00
Joe Cheng
e7ec5e5ba4 Merge pull request #2689 from rstudio/joe/feature/bs4-compat
bs4 compatibility
2020-01-13 11:59:15 -08:00
Carson
03d8a7f296 document with modern roxygen 2020-01-13 13:28:54 -06:00
Carson
480035c065 undo unnecessary diff; yarn build 2020-01-13 13:04:44 -06:00
Winston Chang
b32c18cf72 Merge pull request #2722 from rstudio/appveyor-cache
Bust appveyor cache every time the DESCRIPTION file updates
2020-01-08 14:27:58 -06:00
Barret Schloerke
337a6b276a Add more comments about why we are busting the appveyor cache 2020-01-08 14:57:06 -05:00
Barret Schloerke
06cf1f9477 Add comment about why we are busting the cache 2020-01-08 14:55:12 -05:00
Winston Chang
190cfd2b7a Merge pull request #2721 from rstudio/yarn-frozen-lockfile
Use a frozen lock file within CI
2020-01-08 13:21:47 -06:00
Joe Cheng
63035b4d66 No need for bg-danger, progress-bar-danger is now in bs3compat 2020-01-07 13:55:46 -08:00
Joe Cheng
6a11c8fcb1 Remove .col-form-label
I must've copied this from a bs4 example without understanding
what it's for; it's intended for horizontal form labels, which
we don't do
2020-01-07 13:55:23 -08:00
Joe Cheng
33ffb006e3 Get rid of striped progress bar style
The bs4 striped progress bar directives have changed,
but also the stripes look pretty dated at this point
2020-01-07 13:55:23 -08:00
Joe Cheng
162e7f63a9 Remove branch from htmltools remote 2020-01-07 13:54:55 -08:00
Joe Cheng
bb581eeec4 Remove btn-outline-secondary
We're handling btn-default in bs3compat
2020-01-07 13:54:55 -08:00
Joe Cheng
272c555bc5 Remove unnecessary bs4 compat shims from showcase 2020-01-07 13:54:55 -08:00
Joe Cheng
fb64caab23 Remove unnecessary bs4 classes
Since these classes were added, we've decided to handle more of this
kind of thing in the bootscss package, so bs3 markup can work without
modification in many cases.
2020-01-07 13:54:55 -08:00
Carson
6f2a74a46d requires htmltools >= 0.4.0.9001 2020-01-07 13:54:41 -08:00
Carson
ec65a74492 Need to de-active any nav tab, not just navs in a dropdown 2020-01-07 13:53:57 -08:00
Carson
ba791c42fa showcase compatibility 2020-01-07 13:53:39 -08:00
Carson
5896667c36 For some reason event.relatedTarget isn't populated when the previously active item is a dropdown item 2020-01-07 13:53:39 -08:00
Carson
003c949d38 Compatibility for checkboxes will happen via bs4 shims 2020-01-07 13:53:21 -08:00
Carson
d31394254c progress-bar-striped needs to appear on the actual progress bar (not it's container) 2020-01-07 13:53:21 -08:00
Carson
1a497e246c progress-bar-danger class has been replaced by bg-danger 2020-01-07 13:53:06 -08:00
Carson
d24276aa54 btn-outline-secondary is visually closer to btn-default (compared to btn-secondary) 2020-01-07 13:52:49 -08:00
Carson
6ed21a3e6b dateRangeInput() should use input-group-sm, not input-sm or form-control-sm
The former is supported in both bs3 and bs4 and ensures consistent small sizing of the input groups
2020-01-07 13:52:49 -08:00
Carson
8066f9ce96 remove debugger; more detailed comment 2020-01-07 13:52:49 -08:00
Carson
a0276ec1ce wip bs4 compat work 2020-01-07 13:52:33 -08:00
Joe Cheng
2ab925a24c wip2 2020-01-07 13:52:33 -08:00
Joe Cheng
78fbad7d8d wip1 2020-01-07 13:52:13 -08:00
Alan Dipert
6652ae3042 markdown(): improve docs, simplify call to markdown_html(), default to extensions = TRUE 2020-01-06 22:43:04 +00:00
Alan Dipert
aa12ab7d76 Add shiny::markdown() for inline Markdown 2020-01-04 00:02:30 +00:00
Winston Chang
89be4bdce9 Merge pull request #2728 from rstudio/wch-fix-timer
Timer functions always use ms
2019-12-19 13:12:35 -06:00
Winston Chang
d09a064471 Use test_path 2019-12-13 15:45:17 -06:00
Alan Dipert
2b18ca5a6c .testModule(): remove args parameter fixes #2709 (#2713)
* .testModule(): Don't ignore args parameter, fixes #2709

* .testModule(): eliminate args, rely on dynamic dots

* Expand testModule() dots support, add dynamic dots test

* More ... tests

* testModule(): document dynamic dots

* Tighten up ... docs

* document

* testModule(): sundry improvements to docs and tests
2019-12-13 15:38:45 -06:00
Winston Chang
6bc2f18bbf Timer functions always use ms, and better names. Closes #2725 2019-12-13 15:30:47 -06:00
Alan Dipert
fbb892d84e Add MockShinySession$makeScope() (#2714)
* Add MockShinySession$makeScope(), fixes #2712

* Port some relevant ShinySession$defineOutput() behavior to MockShinySession$defineOutput()

* Add nested module test

* Add test for defineOutput() type check

* minor test improvement

* testModule(): improve inner module test, pass reactive to inner
2019-12-12 16:49:18 -06:00
Barret Schloerke
4efb7c20e4 add install cmd 2019-12-10 12:36:23 -05:00
Barret Schloerke
4beb1f07a6 Bust appveyor cache every time the DESCRIPTION file updates 2019-12-09 15:27:17 -05:00
Barret Schloerke
45e640e5f9 Use a frozen lock file within CI
https://yarnpkg.com/lang/en/docs/cli/install/#toc-yarn-install-frozen-lockfile
> Don’t generate a yarn.lock lockfile and fail if an update is needed.
2019-12-09 15:07:12 -05:00
Barret Schloerke
e84beffee3 Merge pull request #2717 from akgold/label-file-observer
Add label to file reload observer
2019-12-06 14:42:08 -05:00
Winston Chang
e07c7483a7 Merge pull request #2719 from rstudio/wch-fix-odd-chars
Use as.raw instead of charToRaw
2019-12-04 12:31:57 -06:00
Winston Chang
34ec7bf5eb Use as.raw instead of charToRaw
When the package is built, the string with odd characters is marked with the
encoding of the build system. When the build system uses the C locale and the
running system uses a UTF-8 locale (like en_US.UTF-8), this results in a warning
when this function is first accessed. Using as.raw() lets us avoid using a
string altogether.
2019-12-04 10:47:21 -06:00
Barret Schloerke
01b20a4829 display auto reload base directory for reactlog label 2019-12-03 15:16:18 -05:00
Winston Chang
45ea898da4 Rebuild docs with roxygen2 7.0.2 2019-12-03 12:28:06 -06:00
Alex Gold
fd34c5070f Add label to file reload observer 2019-12-02 10:03:11 -05:00
Winston Chang
6c409d96c1 Bump node version on Travis 2019-11-22 14:09:58 -06:00
Winston Chang
0cbe4bb3d4 Rebuild docs with roxygen2 7.0.1 2019-11-22 13:07:44 -06:00
Winston Chang
d04a990235 Merge pull request #2707 from rstudio/wch-outputinfo-null
getCurrentOutputInfo(): return NULL if not in an output
2019-11-22 12:38:05 -06:00
Winston Chang
4747c87632 Rebuild shiny.js 2019-11-21 16:31:51 -06:00
Winston Chang
f57452c7bf Update docs 2019-11-21 16:30:38 -06:00
Winston Chang
9a8e2eb675 getCurrentOutputInfo(): return NULL if not in an output 2019-11-21 16:16:52 -06:00
Winston Chang
8ef7f3cbe2 Merge pull request #2701 from rstudio/wch-update-roxygen
Update roxygen2 to 7.0.0
2019-11-13 15:01:29 -06:00
Winston Chang
de30a65f01 Update documentation of Progress and MockShinySesssion 2019-11-13 15:01:01 -06:00
Winston Chang
0bcf613195 Update to roxygen2 7.0.0 2019-11-13 12:10:57 -06:00
Winston Chang
89fd9004d0 Merge pull request #2585 from rstudio/jeff/feature/test
Add a test runner
2019-11-13 11:53:00 -06:00
trestletech
b2be108db1 Add a message to clarify what app is being tested, support setting progress on mock session. 2019-11-13 11:43:50 -06:00
trestletech
6102c44b70 Attempt to pin roxygen2 2019-11-07 13:26:15 -06:00
trestletech
327cdc8e41 Don't use GH deps for appveyor
see 89feba870d.
2019-11-07 12:51:03 -06:00
trestletech
0bc3613989 PR feedback 2019-11-06 15:58:30 -06:00
Jeff Allen
30cea871f9 Expand testServer/Module docs (#2694)
* Merge and expand testmodule/server docs

* Alos->also
2019-11-06 20:29:44 +00:00
trestletech
5f332fe4db Set working dir in testServer so the app runs in the proper dir. 2019-11-01 16:52:43 -05:00
trestletech
7ee7f2716b Load shiny and inherit from global env. Non-error are no NA and errors are passed through. 2019-11-01 16:40:45 -05:00
trestletech
5e8c39cb1e Add entry in pkgdown 2019-11-01 11:26:23 -05:00
trestletech
ee355200b3 Rename testApp -> runTests 2019-11-01 11:25:47 -05:00
trestletech
986fbe2254 Merge remote-tracking branch 'origin/master' into jeff/feature/test 2019-11-01 11:23:24 -05:00
Jeff Allen
32f93a2be1 Integration Testing Docs (#2691)
* Update roxygen and regenerate.

Mostly just whitespace changes and  `code` -> `verb`.

* R6 documentation for MockShinySession

* Install roxygen from GH

* % are now auto-escaped

(We still need to go find the rest)

* Fixed the ramining \% in roxygen

Found looking for ^#'.*\\% in all R files, so I believe this is all of them.

* Regenerate docs

* Decreate indent in roxygen so paragraphs don't get interpreted as code blocks.

https://github.com/r-lib/roxygen2/issues/948#issuecomment-546386172

* Namespace

* Add MockShinySession reference to pkgdown.

* Clean up test warnings

* Export session
2019-11-01 02:33:58 +00:00
Winston Chang
ab79065c13 Merge pull request #2690 from rstudio/wch-date-color
Datepicker: make disabled months and years lighter gray
2019-10-31 12:16:18 -05:00
Winston Chang
77171b7894 Make cursor indicate when dates are not selectable 2019-10-31 12:09:18 -05:00
Winston Chang
cce8ddb84f Update NEWS 2019-10-30 16:05:55 -05:00
Winston Chang
648b7e5911 Datepicker: make disabled months and years lighter gray 2019-10-30 16:01:29 -05:00
Jeff Allen
67a66fdc93 Merge pull request #2682 from rstudio/jeff/int-test
Introduce integration testing framework
2019-10-30 18:36:44 +00:00
trestletech
5fbaa26d05 Remove vignette. 2019-10-30 11:29:58 -04:00
trestletech
1f4a3c4fd2 Regenerate docs 2019-10-28 23:14:25 -04:00
trestletech
959dc7ffd4 PR feedback 2019-10-28 22:57:30 -04:00
trestletech
0e34221cac How do I still get paid to do this? 2019-10-25 16:54:10 -05:00
trestletech
0cad13b3a3 Placeholder docs for MockShinySession
(More to come in subsequent PR)
2019-10-25 16:47:10 -05:00
trestletech
0776f71ca3 Export session 2019-10-25 16:27:45 -05:00
trestletech
5a74e369ce Implement missing test. 2019-10-25 16:23:16 -05:00
trestletech
799c5ac662 Clean up test warnings 2019-10-25 16:20:33 -05:00
Jeff Allen
1080cf0ef4 Merge pull request #2686 from rstudio/jeff/autoload
Message when autoloading R/ files
2019-10-25 20:16:42 +00:00
trestletech
867d49e3fb Pin to the beginning of the file path. 2019-10-25 15:02:39 -05:00
trestletech
c7be406099 Change URL 2019-10-25 14:21:32 -05:00
trestletech
37257e77ce Disable autoloading with a R/_disable_autoload.R file. 2019-10-25 14:14:46 -05:00
trestletech
270d9ff0fc Add message about loading R/ 2019-10-25 14:02:50 -05:00
trestletech
34b48598d9 Merge remote-tracking branch 'origin/master' into jeff/int-test 2019-10-25 11:23:15 -05:00
trestletech
5105ecb148 Cleaning up the vignette 2019-10-24 14:46:54 -05:00
trestletech
f47b151458 Test improvements for Windows and make CHECK pass. 2019-10-24 11:50:07 -05:00
Jeff Allen
d3f15a58fc Merge pull request #2675 from rstudio/jeff/mock-session
Introduce a MockShinySession object
2019-10-24 15:21:30 +00:00
trestletech
42f6adb7fa Handle Joe's feedback. 2019-10-24 10:20:54 -05:00
trestletech
263f8a8e7d Introduce integration testing functionality 2019-10-24 10:07:23 -05:00
trestletech
3a42d30cfd Simplify run_now() 2019-10-24 09:53:37 -05:00
trestletech
9275217a5a Refine which methods merit warnings 2019-10-22 15:43:26 -05:00
trestletech
1fed19ad68 Export flushReact method 2019-10-22 09:59:20 -05:00
trestletech
6a8a78abd1 Bring in promise helpers and test for getOutput auto-flushing 2019-10-22 09:47:09 -05:00
trestletech
de69f51084 Rename parameter, destroy old outputs 2019-10-22 09:43:37 -05:00
Winston Chang
c81a3f39fd Update NEWS 2019-10-21 16:57:01 -05:00
Winston Chang
6fcb925e34 Merge pull request #2652 from ahmohamed/master
Fix debounce() behavior when r() throws an error
2019-10-21 16:54:59 -05:00
Winston Chang
8823b7280a Merge branch 'master' into hadley-text-doc 2019-10-21 16:45:53 -05:00
Winston Chang
ebadad97a8 Merge pull request #2612 from hadley/file-type
Improve inputFile() accept documentation
2019-10-21 16:43:01 -05:00
Winston Chang
a095c39626 Merge pull request #2616 from hadley/bookmarking-docs
Tweak shinyApp docs about bookmarking
2019-10-21 16:42:07 -05:00
Winston Chang
fb9bcb44c3 Merge branch 'insert-remove-ui' of https://github.com/hadley/shiny into hadley-insert-remove-ui 2019-10-21 16:40:06 -05:00
trestletech
38f593450a PR feedback 2019-10-21 15:35:05 -05:00
trestletech
6d44f2c5cb Align default parameter values with real session 2019-10-21 15:32:37 -05:00
trestletech
d1786a64c4 Try to fix failing Windows test
Perhaps Sys.time isn't high enough resolution there?
2019-10-18 15:33:07 -05:00
trestletech
616ae99c0b Include equality in scheduling comparisons
In real life, the odds that a Sys.time() call is going to hit this equality branch is pretty small as the clock is so precise. However, for testing it's nice to be able to say "this should fire in 10ms, now elapse 10ms" and then confirm that it fired. Without this, you have to pad your delays in order to see the event trigger.
2019-10-18 15:22:03 -05:00
trestletech
4d2ff80788 Introduce MockShinySession 2019-10-18 15:20:44 -05:00
Jeff Allen
005295fd4c Merge pull request #2665 from rstudio/jeff/session-timers
Best-effort task scheduling through the session
2019-10-18 20:12:22 +00:00
trestletech
d6b46f8243 Bring back the good parts of a003 2019-10-18 13:58:42 -05:00
trestletech
bac35e8f1b Revert "PR feedback. Broke tests because of dependency on session, though, so might revert."
This reverts commit a003c4da85.
2019-10-18 13:50:30 -05:00
trestletech
a003c4da85 PR feedback. Broke tests because of dependency on session, though, so might revert. 2019-10-17 14:01:58 -05:00
trestletech
0ae8e4fe8a Consolidate to two Timer classes and simplify conditionals 2019-10-16 14:49:01 -05:00
trestletech
d3667dfc77 Attempt to get the current time from the session, if available. 2019-10-15 15:32:59 -05:00
trestletech
54c5467dc6 Unrelated: fix autoload tests. 2019-10-15 15:02:16 -05:00
trestletech
d01f0300a5 Add mock timer class 2019-10-15 14:59:36 -05:00
trestletech
bff207008f Best-effort task scheduling through the session
Currently a no-op change, as the session just passes through to the global scheduleTask implementation. But this allows us to mock the method for testing.
2019-10-15 14:24:10 -05:00
trestletech
ed739f95ff Revert "Best-effort task scheduling through the session"
This reverts commit bb4de1336c.
2019-10-15 14:22:31 -05:00
trestletech
bb4de1336c Best-effort task scheduling through the session
Currently a no-op change, as the session just passes through to the global scheduleTask implementation. But this allows us to mock the method for testing.
2019-10-15 14:21:44 -05:00
Winston Chang
f7205558d2 Make shiny.autoload.r default to TRUE (#2659)
* Make shiny.autoload.r default to TRUE

* Update comments
2019-10-15 12:24:39 -05:00
Winston Chang
1318544ecf Update NEWS 2019-10-11 14:58:17 -05:00
Winston Chang
a81c161434 Merge pull request #2658 from rstudio/fix-data-table
Preserve matrix dimensions in dataTablesJSON
2019-10-11 14:56:21 -05:00
Winston Chang
73acdc755f Merge pull request #2650 from rstudio/wch-travis-r-versions
Run Travis checks on more R versions
2019-10-10 10:18:21 -05:00
Winston Chang
dd84ea8fda Merge branch 'master' into wch-travis-r-versions 2019-10-10 10:17:33 -05:00
Winston Chang
a2a4e40821 Bump version to 1.4.0.9000 2019-10-10 10:06:12 -05:00
Winston Chang
509f54d68c Merge tag 'v1.4.0'
shiny 1.4.0 on CRAN
2019-10-10 10:04:03 -05:00
Winston Chang
27ce460ea4 Preserve matrix dimensions in dataTablesJSON. Fixes #2653 2019-10-08 16:04:03 -05:00
Ahmed Mohamed
26ba9bf94a Fix debounce() behavior when r() throws an error 2019-10-04 11:58:22 +10:00
Winston Chang
99a7dca3ce Relax test so it passes on R 3.3 and below 2019-09-30 16:28:38 -05:00
Winston Chang
a1a03d94be Run Travis on more R versions 2019-09-30 16:05:31 -05:00
trestletech
85a2d41a72 Merge remote-tracking branch 'origin/master' into jeff/feature/test 2019-09-30 09:55:11 -05:00
Jeff Allen
89bd7e9011 Merge pull request #2647 from rstudio/jeff/cla
Update contributing instructions for cla-assistant.
2019-09-30 14:48:38 +00:00
trestletech
ececdf42a7 Update contributing instructions for cla-assistant. 2019-09-30 09:46:57 -05:00
Jeff Allen
2cf03de8b8 Don't notify slack on success 2019-09-27 18:07:08 +00:00
Winston Chang
c41d38bf61 Document insertUI() and removeUI() together
Since they share so many arguments. I also updated the style of the roxygen comments.
2019-09-17 07:55:53 -05:00
Hadley Wickham
b155e8480b Update docs 2019-09-16 12:30:50 -05:00
Hadley Wickham
e94f687573 Tweak shinyApp docs about bookmarking 2019-09-16 08:00:14 -05:00
Hadley Wickham
5883082d01 Improve inputFile() accept documentation
* accept should be a vector of "unique file type identifiers" not a vector of mime types (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers)

* I updated the example to use req() and to validate the uploaded extension; this is good practice since not all browsers will enforce `accept`
2019-09-15 09:07:16 -05:00
Hadley Wickham
75b53ffda1 Combine documentation for textOutput() and verbatimTextOutput()
And generally polish docs
2019-09-14 09:10:59 -05:00
trestletech
324d9195c3 Merge remote-tracking branch 'origin/wch-slider-phantom' into jeff/feature/test 2019-09-05 16:51:49 -05:00
trestletech
4ad115e024 Load the helpers before the tests -- into an ancestor environment of the tests'. 2019-09-05 09:46:34 -05:00
trestletech
f11d754cfe Add a filter to test 2019-09-05 09:45:06 -05:00
trestletech
65019ce96f Add namespace rewire option.
Return the right structure from shinytest tests and add unit tests.
2019-09-05 09:44:56 -05:00
trestletech
90e8fb2a57 Adding a test runner 2019-09-05 09:44:40 -05:00
575 changed files with 83367 additions and 25023 deletions

View File

@@ -12,7 +12,7 @@
^\.travis\.yml$
^staticdocs$
^tools$
^srcjs$
^srcts$
^CONTRIBUTING.md$
^cran-comments.md$
^.*\.o$

View File

@@ -2,13 +2,15 @@ We welcome contributions to the **shiny** package. To submit a contribution:
1. [Fork](https://github.com/rstudio/shiny/fork) the repository and make your changes.
2. Ensure that you have signed the [individual](https://rstudioblog.files.wordpress.com/2017/05/rstudio_individual_contributor_agreement.pdf) or [corporate](https://rstudioblog.files.wordpress.com/2017/05/rstudio_corporate_contributor_agreement.pdf) contributor agreement as appropriate. You can send the signed copy to jj@rstudio.com.
2. Submit a [pull request](https://help.github.com/articles/using-pull-requests).
3. Submit a [pull request](https://help.github.com/articles/using-pull-requests).
3. Ensure that you have signed the contributor license agreement. It will appear as a "Check"
on your PR and a comment from "CLAassistant" will also appear explaining whether you have
yet to sign. After you sign, you can click the "Recheck" link in that comment and the check
will flip to reflect that you've signed.
We generally do not merge pull requests that update included web libraries (such as Bootstrap or jQuery) because it is difficult for us to verify that the update is done correctly; we prefer to update these libraries ourselves.
## How to make changes
Before you submit a pull request, please do the following:

142
.github/workflows/R-CMD-check.yaml vendored Normal file
View File

@@ -0,0 +1,142 @@
name: R-CMD-check
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
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
id: install-r
with:
r-version: ${{ matrix.config.r }}
- uses: r-lib/actions/setup-pandoc@master
- name: Install pak and query dependencies
shell: Rscript {0}
run: |
install.packages("pak", repos = "https://r-lib.github.io/p/pak/dev/")
saveRDS(pak::pkg_deps_tree("local::.", dependencies = TRUE), ".github/r-depends.rds")
- name: Cache R packages
uses: actions/cache@v2
with:
path: ${{ env.R_LIBS_USER }}
key: ${{ matrix.config.os }}-${{ steps.install-r.outputs.installed-r-version }}-1-${{ hashFiles('.github/r-depends.rds') }}
restore-keys: ${{ matrix.config.os }}-${{ steps.install-r.outputs.installed-r-version }}-1-
- name: Install system dependencies
if: runner.os == 'Linux'
shell: Rscript {0}
run: |
pak::local_system_requirements(execute = TRUE)
# xquartz and cairo are needed for Cairo package.
# harfbuzz and fribidi are needed for textshaping package.
- name: Mac systemdeps
if: runner.os == 'macOS'
run: |
brew install --cask xquartz
brew install cairo
brew install harfbuzz fribidi
# Use a shorter temp directory for pak installations, due to filename
# length issues on Windows. https://github.com/r-lib/pak/issues/252
- name: Windows temp dir
if: runner.os == 'Windows'
run: |
New-Item -Path "C:\" -Name "tmp" -ItemType Directory
echo "TMPDIR=c:\tmp" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Install dependencies
run: |
pak::local_install_dev_deps(upgrade = TRUE)
pak::pkg_install("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@v2
with:
path: ${{ steps.phantomjs.outputs.path }}
key: ${{ matrix.config.os }}-phantomjs
restore-keys: ${{ matrix.config.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: ${{ matrix.config.os }}-r${{ matrix.config.r }}-results
path: check
- name: Fix path for Windows caching
if: runner.os == 'Windows'
# This is needed because if you use the default tar at this stage,
# C:/Rtools/bin/tar.exe, it will say that it can't find gzip.exe. So
# we'll just set the path so that the original tar that would be
# found, will be found.
run: echo "C:/Program Files/Git/usr/bin" >> $GITHUB_PATH

153
.github/workflows/rituals.yaml vendored Normal file
View File

@@ -0,0 +1,153 @@
on:
push:
branches:
- master
- ghactions
pull_request:
branches:
- master
name: Rituals
jobs:
rituals:
name: Rituals
# if: false
runs-on: ${{ matrix.config.os }}
strategy:
fail-fast: false
matrix:
config:
- { os: ubuntu-16.04, r: '4.0', node: "14.x", rspm: "https://packagemanager.rstudio.com/all/__linux__/xenial/latest"}
env:
R_REMOTES_NO_ERRORS_FROM_WARNINGS: true
RSPM: ${{ matrix.config.rspm }}
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v1
- uses: r-lib/actions/pr-fetch@master
name: Git Pull (PR)
if: github.event_name == 'pull_request'
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: r-lib/actions/setup-r@master
id: install-r
with:
r-version: ${{ matrix.config.r }}
- uses: r-lib/actions/setup-pandoc@master
- name: Git Config
run: |
git config user.name "${GITHUB_ACTOR}"
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
- name: Install pak and query dependencies
shell: Rscript {0}
run: |
install.packages("pak", repos = "https://r-lib.github.io/p/pak/dev/")
saveRDS(pak::pkg_deps_tree("local::.", dependencies = TRUE), ".github/r-depends.rds")
- name: Cache R packages
uses: actions/cache@v2
with:
path: ${{ env.R_LIBS_USER }}
key: ${{ matrix.config.os }}-${{ steps.install-r.outputs.installed-r-version }}-1-${{ hashFiles('.github/r-depends.rds') }}
restore-keys: ${{ matrix.config.os }}-${{ steps.install-r.outputs.installed-r-version }}-1-
- name: Install system dependencies
# if: runner.os == 'Linux'
shell: Rscript {0}
run: |
pak::local_system_requirements(execute = TRUE)
- name: Install dependencies
shell: Rscript {0}
run: |
pak::local_install_dev_deps(upgrade = TRUE)
pak::pkg_install("sessioninfo")
pak::pkg_install("devtools")
- name: Session info
shell: Rscript {0}
run: |
options(width = 100)
pkgs <- installed.packages()[, "Package"]
sessioninfo::session_info(pkgs, include_base = TRUE)
- name: Url redirects
# only perform if in an RC branch (`rc-vX.Y.Z`)
if: ${{ github.event_name == 'push' && contains(github.ref, '/rc-v') }}
run: |
Rscript -e 'pak::pkg_install("r-lib/urlchecker"); urlchecker::url_update()'
# throw an error if man files were updated
if [ -n "$(git status --porcelain man)" ]
then
git status --porcelain
>&2 echo "Updated links found in files above"
>&2 echo 'Run `urlchecker::url_update()` to fix links locally'
exit 1
fi
# Add locally changed urls
git add .
git commit -m 'Update links (GitHub Actions)' || echo "No link changes to commit"
- name: Document
run: |
Rscript -e 'devtools::document()'
git add man/\* NAMESPACE
git commit -m 'Document (GitHub Actions)' || echo "No documentation changes to commit"
- name: Check documentation
run: |
./tools/documentation/checkDocsCurrent.sh
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.config.node }}
# 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@v2
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: ${{ matrix.config.os }}-${{ matrix.config.node }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ matrix.config.os }}-${{ matrix.config.node }}-yarn-
- name: Build JS
run: |
cd srcts
tree src
yarn install --immutable && yarn build
git add ./src && git commit -m 'yarn lint (GitHub Actions)' || echo "No yarn lint changes to commit"
git add ../inst && git commit -m 'yarn build (GitHub Actions)' || echo "No yarn build changes to commit"
- name: Check JS build is latest
run: |
./tools/checkJSCurrent.sh
- name: Git Push (PR)
uses: r-lib/actions/pr-push@master
if: github.event_name == 'pull_request'
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Git Push (MASTER)
if: github.event_name == 'push'
run: |
git push https://${{github.actor}}:${{secrets.GITHUB_TOKEN}}@github.com/${{github.repository}}.git HEAD:${{ github.ref }} || echo "No changes to push"
# Execute after pushing, as no updated files will be produced
- name: Test TypeScript code
run: |
cd srcts
yarn test

3
.gitignore vendored
View File

@@ -10,3 +10,6 @@ shinyapps/
README.html
.*.Rnb.cached
tools/yarn-error.log
# GHA remotes installation
.github/r-depends.rds

View File

@@ -1,31 +0,0 @@
language: r
matrix:
include:
- name: "Roxygen check"
r: release
r_packages:
- devtools
- roxygen2
script: ./tools/checkDocsCurrent.sh
- name: "Javascript check"
language: node_js
cache: yarn
script: ./tools/checkJSCurrent.sh
node_js:
- "10"
- name: "Old Release Check"
r: oldrel
- name: "Current Release Check"
r: release
- name: "Development Release Check"
r: devel
sudo: false
cache: packages
notifications:
email:
on_success: change
on_failure: change
slack:
secure: QoM0+hliVC4l2HYv126AkljG/uFvgwayW9IpuB5QNqjSukM122MhMDL7ZuMB9a2vWP24juzOTXiNIymgEspfnvvAMnZwYRBNWkuot2m8HIR2B9UjQLiztFnN1EAT+P+thz8Qax9TV2SOfXb2S2ZOeZmRTVkJctxkL8heAZadIC4=
on_pull_requests: false

View File

@@ -1,13 +1,18 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 1.4.0
Version: 1.6.0.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("Carson", "Sievert", role = "aut", email = "carson@rstudio.com"),
person("Barret", "Schloerke", role = "aut", email = "barret@rstudio.com"),
person("Yihui", "Xie", role = "aut", email = "yihui@rstudio.com"),
person("Jeff", "Allen", role = "aut", email = "jeff@rstudio.com"),
person("Jonathan", "McPherson", role = "aut", email = "jonathan@rstudio.com"),
person("Alan", "Dipert", role = "aut"),
person("Barbara", "Borges", role = "aut"),
person(family = "RStudio", role = "cph"),
person(family = "jQuery Foundation", role = "cph",
comment = "jQuery library and jQuery UI library"),
@@ -23,10 +28,18 @@ Authors@R: c(
comment = "Bootstrap library"),
person(family = "Twitter, Inc", role = "cph",
comment = "Bootstrap library"),
person("Alexander", "Farkas", role = c("ctb", "cph"),
comment = "html5shiv library"),
person("Scott", "Jehl", role = c("ctb", "cph"),
comment = "Respond.js library"),
person("Prem Nawaz", "Khan", role = "ctb",
comment = "Bootstrap accessibility plugin"),
person("Victor", "Tsaran", role = "ctb",
comment = "Bootstrap accessibility plugin"),
person("Dennis", "Lembree", role = "ctb",
comment = "Bootstrap accessibility plugin"),
person("Srinivasu", "Chakravarthula", role = "ctb",
comment = "Bootstrap accessibility plugin"),
person("Cathy", "O'Connor", role = "ctb",
comment = "Bootstrap accessibility plugin"),
person(family = "PayPal, Inc", role = "cph",
comment = "Bootstrap accessibility plugin"),
person("Stefan", "Petre", role = c("ctb", "cph"),
comment = "Bootstrap-datepicker library"),
person("Andrew", "Rowls", role = c("ctb", "cph"),
@@ -35,10 +48,8 @@ Authors@R: c(
comment = "Font-Awesome font"),
person("Brian", "Reavis", role = c("ctb", "cph"),
comment = "selectize.js library"),
person("Kristopher Michael", "Kowal", role = c("ctb", "cph"),
comment = "es5-shim library"),
person(family = "es5-shim contributors", role = c("ctb", "cph"),
comment = "es5-shim library"),
person("Salmen", "Bejaoui", role = c("ctb", "cph"),
comment = "selectize-plugin-a11y library"),
person("Denis", "Ineshin", role = c("ctb", "cph"),
comment = "ion.rangeSlider library"),
person("Sami", "Samhuri", role = c("ctb", "cph"),
@@ -69,45 +80,58 @@ Imports:
mime (>= 0.3),
jsonlite (>= 0.9.16),
xtable,
digest,
htmltools (>= 0.4.0),
htmltools (>= 0.5.0.9001),
R6 (>= 2.0),
sourcetools,
later (>= 1.0.0),
promises (>= 1.1.0),
tools,
crayon,
rlang (>= 0.4.0),
fastmap (>= 1.0.0)
rlang (>= 0.4.10),
fastmap (>= 1.1.0),
withr,
commonmark (>= 1.7),
glue (>= 1.3.2),
bslib (>= 0.2.2.9002),
cachem,
ellipsis,
lifecycle (>= 0.2.0)
Suggests:
datasets,
Cairo (>= 1.5-5),
testthat (>= 2.1.1),
testthat (>= 3.0.0),
knitr (>= 1.6),
markdown,
rmarkdown,
ggplot2,
reactlog (>= 1.0.0),
magrittr,
yaml
URL: http://shiny.rstudio.com
shinytest (>= 1.4.0.9003),
yaml,
future,
dygraphs,
ragg,
showtext,
sass
URL: https://shiny.rstudio.com/
BugReports: https://github.com/rstudio/shiny/issues
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'
'cache-utils.R'
'deprecated.R'
'devmode.R'
'diagnose.R'
'fileupload.R'
'font-awesome.R'
@@ -117,7 +141,6 @@ Collate:
'history.R'
'hooks.R'
'html-deps.R'
'htmltools.R'
'image-interact-opts.R'
'image-interact.R'
'imageutils.R'
@@ -139,31 +162,45 @@ Collate:
'insert-tab.R'
'insert-ui.R'
'jqueryui.R'
'knitr.R'
'middleware-shiny.R'
'middleware.R'
'timer.R'
'shiny.R'
'mock-session.R'
'modal.R'
'modules.R'
'notifications.R'
'priorityqueue.R'
'progress.R'
'react.R'
'reexports.R'
'render-cached-plot.R'
'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.R'
'shiny-package.R'
'shinyapp.R'
'shinyui.R'
'shinywrappers.R'
'showcase.R'
'snapshot.R'
'tar.R'
'test-export.R'
'timer.R'
'test-server.R'
'test.R'
'update-input.R'
RoxygenNote: 6.1.1
'utils-lang.R'
'version_jquery.R'
'viewer.R'
RoxygenNote: 7.1.1
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RdMacros: lifecycle
Config/testthat/edition: 3

449
LICENSE
View File

@@ -8,12 +8,11 @@ these components are included below):
- jQuery, https://github.com/jquery/jquery
- jQuery UI (some components), https://github.com/jquery/jquery-ui
- Bootstrap, https://github.com/twbs/bootstrap
- html5shiv, https://github.com/aFarkas/html5shiv
- Respond.js, https://github.com/scottjehl/Respond
- bootstrap-accessibility-plugin, https://github.com/paypal/bootstrap-accessibility-plugin
- bootstrap-datepicker, https://github.com/eternicode/bootstrap-datepicker
- Font Awesome, https://github.com/FortAwesome/Font-Awesome
- selectize.js, https://github.com/selectize/selectize.js
- es5-shim, https://github.com/es-shims/es5-shim
- selectize-plugin-a11y, https://github.com/SLMNBJ/selectize-plugin-a11y
- ion.rangeSlider, https://github.com/IonDen/ion.rangeSlider
- strftime for Javascript, https://github.com/samsonjs/strftime
- DataTables, https://github.com/DataTables/DataTables
@@ -72,399 +71,35 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
html5shiv License (MIT and GPL-2)
bootstrap-accessibility-plugin (BSD-3-Clause License)
----------------------------------------------------------------------
Copyright (c) 2014 Alexander Farkas (aFarkas).
Licensed under MIT
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.
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<Html5shiv, The HTML5 Shiv enables use of HTML5 sectioning elements in
legacy Internet Explorer and provides basic HTML5 styling for Internet Explorer 6-9,
Safari 4.x (and iPhone 3.x), and Firefox 3.x.>
Copyright (C) 2014 Alexander Farkas (aFarkas)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) 2014 Alexander Farkas (aFarkas)
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General
Public License instead of this License.
Respond.js License
----------------------------------------------------------------------
Copyright (c) 2012 Scott Jehl
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.
Copyright (c) 2014, PayPal
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of the PayPal nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
bootstrap-datepicker
@@ -700,7 +335,7 @@ THE SOFTWARE.
----
Copyright (c) 2014, Dave Gandy http://fontawesome.io/,
Copyright (c) 2014, Dave Gandy http://fontawesome.com/,
with Reserved Font Name Font Awesome.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
@@ -1322,30 +957,18 @@ selectize.js
limitations under the License.
es5-shim License
selectize-plugin-a11y License
----------------------------------------------------------------------
The MIT License (MIT)
Copyright (C) 2009-2014 Kristopher Michael Kowal and contributors
Copyright 2018-present Salmen Bejaoui
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:
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 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.
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.
ion.rangeSlider License

112
NAMESPACE
View File

@@ -1,15 +1,18 @@
# Generated by roxygen2: do not edit by hand
S3method("$",mockclientdata)
S3method("$",reactivevalues)
S3method("$",session_proxy)
S3method("$",shinyoutput)
S3method("$<-",reactivevalues)
S3method("$<-",session_proxy)
S3method("$<-",shinyoutput)
S3method("[",mockclientdata)
S3method("[",reactivevalues)
S3method("[",shinyoutput)
S3method("[<-",reactivevalues)
S3method("[<-",shinyoutput)
S3method("[[",mockclientdata)
S3method("[[",reactivevalues)
S3method("[[",session_proxy)
S3method("[[",shinyoutput)
@@ -22,6 +25,22 @@ 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(names,reactivevalues)
@@ -29,11 +48,13 @@ S3method(print,reactive)
S3method(print,reactivevalues)
S3method(print,shiny.appobj)
S3method(print,shiny.render.function)
S3method(print,shiny_runtests)
S3method(str,reactivevalues)
export("conditionStackTrace<-")
export(..stacktraceoff..)
export(..stacktraceon..)
export(HTML)
export(MockShinySession)
export(NS)
export(Progress)
export(a)
@@ -45,6 +66,8 @@ export(animationOptions)
export(appendTab)
export(as.shiny.appobj)
export(basicPage)
export(bindCache)
export(bindEvent)
export(bookmarkButton)
export(bootstrapLib)
export(bootstrapPage)
@@ -68,6 +91,7 @@ export(dateInput)
export(dateRangeInput)
export(dblclickOpts)
export(debounce)
export(devmode)
export(dialogViewer)
export(diskCache)
export(div)
@@ -94,10 +118,12 @@ 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)
@@ -111,9 +137,11 @@ export(hoverOpts)
export(hr)
export(htmlOutput)
export(htmlTemplate)
export(httpResponse)
export(icon)
export(imageOutput)
export(img)
export(in_devmode)
export(incProgress)
export(includeCSS)
export(includeHTML)
@@ -138,10 +166,12 @@ export(loadSupport)
export(mainPanel)
export(makeReactiveBinding)
export(markRenderFunction)
export(markdown)
export(maskReactiveContext)
export(memoryCache)
export(modalButton)
export(modalDialog)
export(moduleServer)
export(navbarMenu)
export(navbarPage)
export(navlistPanel)
@@ -172,8 +202,10 @@ export(pre)
export(prependTab)
export(printError)
export(printStackTrace)
export(quoToFunction)
export(radioButtons)
export(reactive)
export(reactiveConsole)
export(reactiveFileReader)
export(reactivePlot)
export(reactivePoll)
@@ -189,6 +221,7 @@ export(reactlog)
export(reactlogReset)
export(reactlogShow)
export(registerInputHandler)
export(registerThemeDependency)
export(removeInputHandler)
export(removeModal)
export(removeNotification)
@@ -212,6 +245,7 @@ export(runExample)
export(runGadget)
export(runGist)
export(runGitHub)
export(runTests)
export(runUrl)
export(safeError)
export(selectInput)
@@ -223,6 +257,7 @@ export(setSerializer)
export(shinyApp)
export(shinyAppDir)
export(shinyAppFile)
export(shinyAppTemplate)
export(shinyOptions)
export(shinyServer)
export(shinyUI)
@@ -246,6 +281,7 @@ export(strong)
export(submitButton)
export(suppressDependencies)
export(tabPanel)
export(tabPanelBody)
export(tableOutput)
export(tabsetPanel)
export(tag)
@@ -257,6 +293,7 @@ export(tagHasAttribute)
export(tagList)
export(tagSetChildren)
export(tags)
export(testServer)
export(textAreaInput)
export(textInput)
export(textOutput)
@@ -264,6 +301,7 @@ export(throttle)
export(titlePanel)
export(uiOutput)
export(updateActionButton)
export(updateActionLink)
export(updateCheckboxGroupInput)
export(updateCheckboxInput)
export(updateDateInput)
@@ -294,15 +332,87 @@ export(withMathJax)
export(withProgress)
export(withReactiveDomain)
export(withTags)
export(with_devmode)
import(R6)
import(digest)
import(htmltools)
import(httpuv)
import(methods)
import(mime)
import(xtable)
importFrom(ellipsis,check_dots_empty)
importFrom(ellipsis,check_dots_unnamed)
importFrom(fastmap,fastmap)
importFrom(fastmap,is.key_missing)
importFrom(fastmap,key_missing)
importFrom(grDevices,dev.cur)
importFrom(grDevices,dev.set)
importFrom(htmltools,HTML)
importFrom(htmltools,a)
importFrom(htmltools,br)
importFrom(htmltools,code)
importFrom(htmltools,div)
importFrom(htmltools,em)
importFrom(htmltools,h1)
importFrom(htmltools,h2)
importFrom(htmltools,h3)
importFrom(htmltools,h4)
importFrom(htmltools,h5)
importFrom(htmltools,h6)
importFrom(htmltools,hr)
importFrom(htmltools,htmlTemplate)
importFrom(htmltools,img)
importFrom(htmltools,includeCSS)
importFrom(htmltools,includeHTML)
importFrom(htmltools,includeMarkdown)
importFrom(htmltools,includeScript)
importFrom(htmltools,includeText)
importFrom(htmltools,is.singleton)
importFrom(htmltools,p)
importFrom(htmltools,pre)
importFrom(htmltools,singleton)
importFrom(htmltools,span)
importFrom(htmltools,strong)
importFrom(htmltools,suppressDependencies)
importFrom(htmltools,tag)
importFrom(htmltools,tagAppendAttributes)
importFrom(htmltools,tagAppendChild)
importFrom(htmltools,tagAppendChildren)
importFrom(htmltools,tagGetAttribute)
importFrom(htmltools,tagHasAttribute)
importFrom(htmltools,tagList)
importFrom(htmltools,tagSetChildren)
importFrom(htmltools,tags)
importFrom(htmltools,validateCssUnit)
importFrom(htmltools,withTags)
importFrom(lifecycle,deprecated)
importFrom(promises,"%...!%")
importFrom(promises,"%...>%")
importFrom(promises,as.promise)
importFrom(promises,is.promising)
importFrom(promises,promise)
importFrom(promises,promise_reject)
importFrom(promises,promise_resolve)
importFrom(rlang,"%||%")
importFrom(rlang,as_function)
importFrom(rlang,as_quosure)
importFrom(rlang,enexpr)
importFrom(rlang,enquo)
importFrom(rlang,enquos)
importFrom(rlang,enquos0)
importFrom(rlang,eval_tidy)
importFrom(rlang,expr)
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,zap_srcref)

670
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
}

293
R/app_template.R Normal file
View File

@@ -0,0 +1,293 @@
#' Generate a Shiny application from a template
#'
#' This function populates a directory with files for a Shiny application.
#'
#' In an interactive R session, this function will, by default, prompt the user
#' to select which components to add to the application. Choices are
#'
#' ```
#' 1: All
#' 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
#' ```
#'
#' If option 1 is selected, the full example application including the
#' following files and directories is created:
#'
#' ```
#' appdir/
#' |- app.R
#' |- R
#' | |- example-module.R
#' | `- example.R
#' `- tests
#' |- shinytest.R
#' |- shinytest
#' | `- mytest.R
#' |- testthat.R
#' `- testthat
#' |- test-examplemodule.R
#' |- test-server.R
#' `- test-sort.R
#' ```
#'
#' Some notes about these files:
#' * `app.R` is the main application file.
#' * All files in the `R/` subdirectory are automatically sourced when the
#' application is run.
#' * `R/example.R` and `R/example-module.R` are automatically sourced when
#' the application is run. The first contains a function `lexical_sort()`,
#' and the second contains code for module created by the
#' [moduleServer()] function, which is used in the application.
#' * `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/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-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
#' 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
#' directory. With "all", all template items will be added to the app
#' directory.
#' @param dryrun If `TRUE`, don't actually write any files; just print out which
#' files would be written.
#'
#' @export
shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
{
if (is.null(path)) {
stop("Please provide a `path`.")
}
# =======================================================
# Option handling
# =======================================================
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"
)
if (identical(examples, "default")) {
if (interactive()) {
examples <- "ask"
} else {
examples <- "all"
}
}
if (!identical(examples, "ask") &&
!identical(examples, "all") &&
any(! examples %in% names(choices)))
{
stop('`examples` must be one of "default", "ask", "all", or any combination of "',
paste(names(choices), collapse = '", "'), '".')
}
if (identical(examples, "ask")) {
response <- select_menu(
c(all = "All", choices),
title = paste0(
"Select which of the following to add at ", path, "/ :"
),
msg = "Enter one or more numbers (with spaces), or an empty line to exit: \n"
)
examples <- names(response)
}
examples <- unique(examples)
if ("all" %in% examples) {
examples <- names(choices)
}
if (length(examples) == 0) {
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.")
}
}
}
# =======================================================
# Utility functions
# =======================================================
# Check if a directory is empty, ignoring certain files
dir_is_empty <- function(path) {
files <- list.files(path, all.files = TRUE, no.. = TRUE)
# Ignore .DS_Store files, which are sometimes automatically created on macOS
files <- setdiff(files, ".DS_Store")
return(length(files) != 0)
}
# Helper to resolve paths relative to our template
template_path <- function(...) {
system.file("app_template", ..., package = "shiny")
}
# Resolve path relative to destination
dest_path <- function(...) {
file.path(path, ...)
}
mkdir <- function(path) {
if (!dirExists(path)) {
message("Creating ", ensure_trailing_slash(path))
if (!dryrun) {
dir.create(path, recursive = TRUE)
}
}
}
# Copy a file from the template directory to the destination directory. If the
# file has templating code (it contains `{{` in the text), then run it through
# the htmlTemplate().
copy_file_one <- function(name) {
from <- template_path(name)
to <- dest_path(name)
message("Creating ", to)
if (file.exists(to)) {
stop(to, " already exists. Please remove it and try again.", call. = FALSE)
}
if (!dryrun) {
is_template <- any(grepl("{{", readLines(from), fixed = TRUE))
if (is_template) {
writeChar(
as.character(htmlTemplate(
from,
rdir = "rdir" %in% examples,
module = "module" %in% examples
)),
con = to,
eos = NULL
)
} else {
file.copy(from, to)
}
}
}
# Copy multiple files from template to destination.
copy_file <- function(names) {
for (name in names) {
copy_file_one(name)
}
}
# Copy the files for a tests/ subdirectory
copy_test_dir <- function(name) {
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")
files <- files[!is_r_folder_file]
}
# Filter out module files, if applicable.
if (! "module" %in% examples) {
files <- files[!grepl("module", files)]
}
mkdir(dest_path("tests"))
# Create any subdirectories if needed
dirs <- setdiff(unique(dirname(files)), ".")
for (dir in dirs) {
mkdir(dest_path("tests", dir))
}
copy_file(file.path("tests", files))
}
# =======================================================
# Main function
# =======================================================
if (is.null(path)) {
stop("`path` is missing.")
}
if (file.exists(path) && !dirExists(path)) {
stop(path, " exists but is not a directory.")
}
if (dirExists(path) && dir_is_empty(path)) {
if (interactive()) {
response <- readline(paste0(
ensure_trailing_slash(path),
" is not empty. Do you want to use this directory anyway? [y/n] "
))
if (tolower(response) != "y") {
return(invisible())
}
}
} else {
mkdir(path)
}
if ("app" %in% examples) {
copy_file("app.R")
}
# R/ dir with non-module files
if ("rdir" %in% examples) {
files <- dir(template_path("R"))
non_module_files <- files[!grepl("module.R$", files)]
mkdir(dest_path("R"))
copy_file(file.path("R", non_module_files))
}
# R/ dir with module files
if ("module" %in% examples) {
files <- dir(template_path("R"))
module_files <- files[grepl("module.R$", files)]
mkdir(dest_path("R"))
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")
}
invisible()
}

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

@@ -0,0 +1,774 @@
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
#' `self$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 [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 [markRenderFunction()]
#' (instead of [createRenderFunction()]). Because the user code is wrapped in
#' another function, `markRenderFunction()` is not able to automatically
#' extract the user-provided code and use it in the cache key. Instead,
#' `renderPrint` calls `markRenderFunction()`, 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) {
#' expr <- substitute(expr)
#'
#' htmlwidgets::shinyRenderWidget(expr,
#' myWidgetOutput,
#' quoted = TRUE,
#' env = parent.frame(),
#' cacheHint = list(label = "myWidget", userExpr = expr)
#' )
#' }
#' ```
#'
#' If your `render` function sets any internal state, you may find it useful
#' in your call to [createRenderFunction()] or [markRenderFunction()] 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 depdency 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()
label <- exprToLabel(substitute(key), "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)
# 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)) {
rm(list = ".", envir = .GenericCallEnv)
}
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))
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, ...)
)
}
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()
})
# 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
}

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

@@ -0,0 +1,315 @@
#' 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)
label <- label %||%
sprintf('bindEvent(%s, %s)', attr(x, "observable", exact = TRUE)$.label, quos_to_label(qs))
# Don't hold on to the reference for x, so that it can be GC'd
rm(x)
initialized <- FALSE
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", class(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(...))
}
)
}
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.
x$.label <- label %||% sprintf('bindEvent(%s, %s)', x$.label, quos_to_label(qs))
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))
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()) {
@@ -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()) {
@@ -431,8 +444,8 @@ RestoreInputSet <- R6Class("RestoreInputSet",
)
)
restoreCtxStack <- Stack$new()
# This is a fastmap::faststack(); value is assigned in .onLoad().
restoreCtxStack <- NULL
withRestoreContext <- function(ctx, expr) {
restoreCtxStack$push(ctx)
@@ -453,7 +466,7 @@ hasCurrentRestoreContext <- function() {
domain <- getDefaultReactiveDomain()
if (!is.null(domain) && !is.null(domain$restoreContext))
return(TRUE)
return(FALSE)
}
@@ -1144,10 +1157,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 +1183,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

@@ -13,9 +13,7 @@
#' 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.
#'
@@ -25,7 +23,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 +85,12 @@
#' }
#' @rdname fluidPage
#' @export
fluidPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
fluidPage <- function(..., title = NULL, responsive = deprecated(), theme = NULL, lang = NULL) {
bootstrapPage(div(class = "container-fluid", ...),
title = title,
responsive = responsive,
theme = theme)
theme = theme,
lang = lang)
}
@@ -115,9 +114,7 @@ fluidRow <- function(...) {
#' @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.
#'
@@ -128,7 +125,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 +153,12 @@ fluidRow <- function(...) {
#'
#' @rdname fixedPage
#' @export
fixedPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
fixedPage <- function(..., title = NULL, responsive = deprecated(), theme = NULL, lang = NULL) {
bootstrapPage(div(class = "container", ...),
title = title,
responsive = responsive,
theme = theme)
theme = theme,
lang = lang)
}
#' @rdname fixedPage
@@ -231,8 +229,12 @@ column <- function(width, ..., offset = 0) {
stop("column width must be between 1 and 12")
colClass <- paste0("col-sm-", width)
if (offset > 0)
colClass <- paste0(colClass, " col-sm-offset-", offset)
if (offset > 0) {
# offset-md-x is for bootstrap 4 forward compat
# (every size tier has been bumped up one level)
# https://github.com/twbs/bootstrap/blob/74b8fe7/docs/4.3/migration/index.html#L659
colClass <- paste0(colClass, " offset-md-", offset, " col-sm-offset-", offset)
}
div(class = colClass, ...)
}
@@ -351,6 +353,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",
...
)
)
@@ -360,6 +364,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",
...
)
}
@@ -428,7 +434,7 @@ verticalLayout <- function(..., fluid = TRUE) {
flowLayout <- function(..., cellArgs = list()) {
children <- list(...)
childIdx <- !nzchar(names(children) %OR% character(length(children)))
childIdx <- !nzchar(names(children) %||% character(length(children)))
attribs <- children[!childIdx]
children <- children[childIdx]
@@ -511,7 +517,7 @@ inputPanel <- function(...) {
splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
children <- list(...)
childIdx <- !nzchar(names(children) %OR% character(length(children)))
childIdx <- !nzchar(names(children) %||% character(length(children)))
attribs <- children[!childIdx]
children <- children[childIdx]
count <- length(children)
@@ -566,7 +572,7 @@ splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
#' @param flex Determines how space should be distributed to the cells. Can be a
#' single value like `1` or `2` to evenly distribute the available
#' space; or use a vector of numbers to specify the proportions. For example,
#' `flex = c(2, 3)` would cause the space to be split 40\%/60\% between
#' `flex = c(2, 3)` would cause the space to be split 40%/60% between
#' two cells. NA values will cause the corresponding cell to be sized
#' according to its contents (without growing or shrinking).
#' @param width,height The total amount of width and height to use for the
@@ -689,37 +695,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

@@ -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,18 +1,25 @@
#' @importFrom fastmap key_missing
#' @export
fastmap::key_missing
#' @importFrom fastmap is.key_missing
#' @export
fastmap::is.key_missing
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

@@ -133,7 +133,7 @@ captureStackTraces <- function(expr) {
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(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
@@ -217,7 +217,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(
@@ -264,34 +264,34 @@ 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)) {
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
@@ -301,7 +301,7 @@ printStackTrace <- function(cond,
stackTraceCallNames <- mapply(stackTraceCallNames, FUN = `[`, toKeep, SIMPLIFY = FALSE)
stackTraceParents <- mapply(stackTraceParents, FUN = `[`, toKeep, SIMPLIFY = FALSE)
}
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
@@ -310,7 +310,7 @@ printStackTrace <- function(cond,
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.
@@ -320,7 +320,7 @@ printStackTrace <- function(cond,
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)),
@@ -329,7 +329,7 @@ printStackTrace <- function(cond,
category = rev(getCallCategories(calls[index])),
stringsAsFactors = FALSE
)
if (i != 1) {
message("From earlier call:")
}
@@ -357,7 +357,7 @@ printStackTrace <- function(cond,
st
}, SIMPLIFY = FALSE)
invisible()
}
@@ -370,12 +370,13 @@ printStackTrace <- function(cond,
#' @rdname stacktrace
#' @export
extractStackTrace <- function(calls,
full = getOption("shiny.fullstacktrace", FALSE),
full = get_devmode_option("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")
shinyDeprecated(
"1.0.5", "extractStackTrace()",
details = "Please contact the Shiny team if you were using this functionality."
)
srcrefs <- getSrcRefs(calls)
if (offset) {
@@ -459,19 +460,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,11 +487,11 @@ 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.
@@ -502,7 +503,7 @@ pruneStackTrace <- function(parents) {
FALSE
}
}, FUN.VALUE = logical(1))
include
}
@@ -515,7 +516,7 @@ 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)
@@ -530,10 +531,10 @@ 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
}
@@ -544,13 +545,14 @@ offsetSrcrefs <- function(calls, offset = TRUE) {
#' @rdname stacktrace
#' @export
formatStackTrace <- function(calls, indent = " ",
full = getOption("shiny.fullstacktrace", FALSE),
full = get_devmode_option("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")
shinyDeprecated(
"1.0.5", "formatStackTrace()",
details = "Please contact the Shiny team if you were using this functionality."
)
st <- extractStackTrace(calls, full = full, offset = offset)
if (nrow(st) == 0) {
return(character(0))

108
R/deprecated.R Normal file
View File

@@ -0,0 +1,108 @@
#' 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
) {
if (is_false(getOption("shiny.deprecation.messages"))) {
return(invisible())
}
msg <- paste0("`", what, "` is deprecated 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())
# manually
msg <- paste0(
"The `env` and `quoted` arguments are deprecated as of shiny 1.6.0.",
" Please use quosures from `rlang` instead.\n",
"See <https://github.com/rstudio/shiny/issues/3108> for more information."
)
rlang::inform(message = msg, .frequency = "always", .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 (lifecycle::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 (lifecycle::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
)
}

358
R/devmode.R Normal file
View File

@@ -0,0 +1,358 @@
#' Shiny Developer Mode
#'
#' @description \lifecycle{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")
}
#' @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,
...
)
}
#' @include map.R
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.
#' @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
)
})
)
}
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
)

View File

@@ -1,75 +1,461 @@
# Generated by tools/updateFontAwesome.R: do not edit by hand
font_awesome_brands <- c(
"500px", "accessible-icon", "accusoft", "adn", "adversal",
"affiliatetheme", "algolia", "alipay", "amazon", "amazon-pay",
"amilia", "android", "angellist", "angrycreative", "angular",
"app-store", "app-store-ios", "apper", "apple", "apple-pay",
"asymmetrik", "audible", "autoprefixer", "avianex", "aviato",
"aws", "bandcamp", "behance", "behance-square", "bimobject",
"bitbucket", "bitcoin", "bity", "black-tie", "blackberry", "blogger",
"blogger-b", "bluetooth", "bluetooth-b", "btc", "buromobelexperte",
"buysellads", "cc-amazon-pay", "cc-amex", "cc-apple-pay", "cc-diners-club",
"cc-discover", "cc-jcb", "cc-mastercard", "cc-paypal", "cc-stripe",
"cc-visa", "centercode", "chrome", "cloudscale", "cloudsmith",
"cloudversify", "codepen", "codiepie", "connectdevelop", "contao",
"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", "css3", "css3-alt", "cuttlefish", "d-and-d",
"dashcube", "delicious", "deploydog", "deskpro", "deviantart",
"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", "expeditedssl",
"facebook", "facebook-f", "facebook-messenger", "facebook-square",
"firefox", "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-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", "imdb", "instagram", "internet-explorer",
"ioxhost", "itunes", "itunes-note", "java", "jedi-order", "jenkins",
"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", "medapps", "medium",
"medium-m", "medrt", "meetup", "megaport", "microsoft", "mix",
"mixcloud", "mizuni", "modx", "monero", "napster", "neos", "nimblr",
"nintendo-switch", "node", "node-js", "npm", "ns8", "nutritionix",
"odnoklassniki", "odnoklassniki-square", "old-republic", "opencart",
"openid", "opera", "optin-monster", "osi", "page4", "pagelines",
"palfed", "patreon", "paypal", "periscope", "phabricator", "phoenix-framework",
"phoenix-squadron", "php", "pied-piper", "pied-piper-alt", "pied-piper-hat",
"pied-piper-pp", "pinterest", "pinterest-p", "pinterest-square",
"playstation", "product-hunt", "pushed", "python", "qq", "quinscape",
"quora", "r-project", "ravelry", "react", "readme", "rebel",
"red-river", "reddit", "reddit-alien", "reddit-square", "rendact",
"renren", "replyd", "researchgate", "resolving", "rev", "rocketchat",
"rockrms", "safari", "sass", "schlix", "scribd", "searchengin",
"sellcast", "sellsy", "servicestack", "shirtsinbulk", "shopware",
"simplybuilt", "sistrix", "sith", "skyatlas", "skype", "slack",
"slack-hash", "slideshare", "snapchat", "snapchat-ghost", "snapchat-square",
"soundcloud", "speakap", "spotify", "squarespace", "stack-exchange",
"stack-overflow", "staylinked", "steam", "steam-square", "steam-symbol",
"sticker-mule", "strava", "stripe", "stripe-s", "studiovinari",
"stumbleupon", "stumbleupon-circle", "superpowers", "supple",
"teamspeak", "telegram", "telegram-plane", "tencent-weibo", "the-red-yeti",
"themeco", "themeisle", "trade-federation", "trello", "tripadvisor",
"tumblr", "tumblr-square", "twitch", "twitter", "twitter-square",
"typo3", "uber", "uikit", "uniregistry", "untappd", "usb", "ussunnah",
"vaadin", "viacoin", "viadeo", "viadeo-square", "viber", "vimeo",
"vimeo-square", "vimeo-v", "vine", "vk", "vnv", "vuejs", "weebly",
"weibo", "weixin", "whatsapp", "whatsapp-square", "whmcs", "wikipedia-w",
"windows", "wix", "wolf-pack-battalion", "wordpress", "wordpress-simple",
"wpbeginner", "wpexplorer", "wpforms", "xbox", "xing", "xing-square",
"y-combinator", "yahoo", "yandex", "yandex-international", "yelp",
"yoast", "youtube", "youtube-square", "zhihu"
)
"500px",
"accessible-icon",
"accusoft",
"acquisitions-incorporated",
"adn",
"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",
"cloudflare",
"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",
"deezer",
"delicious",
"deploydog",
"deskpro",
"dev",
"deviantart",
"dhl",
"diaspora",
"digg",
"digital-ocean",
"discord",
"discourse",
"dochub",
"docker",
"draft2digital",
"dribbble",
"dribbble-square",
"dropbox",
"drupal",
"dyalog",
"earlybirds",
"ebay",
"edge",
"edge-legacy",
"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-pay",
"google-play",
"google-plus",
"google-plus-g",
"google-plus-square",
"google-wallet",
"gratipay",
"grav",
"gripfire",
"grunt",
"guilded",
"gulp",
"hacker-news",
"hacker-news-square",
"hackerrank",
"hips",
"hire-a-helper",
"hive",
"hooli",
"hornbill",
"hotjar",
"houzz",
"html5",
"hubspot",
"ideal",
"imdb",
"innosoft",
"instagram",
"instagram-square",
"instalod",
"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",
"octopus-deploy",
"odnoklassniki",
"odnoklassniki-square",
"old-republic",
"opencart",
"openid",
"opera",
"optin-monster",
"orcid",
"osi",
"page4",
"pagelines",
"palfed",
"patreon",
"paypal",
"penny-arcade",
"perbyte",
"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",
"rust",
"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",
"tiktok",
"trade-federation",
"trello",
"tripadvisor",
"tumblr",
"tumblr-square",
"twitch",
"twitter",
"twitter-square",
"typo3",
"uber",
"ubuntu",
"uikit",
"umbraco",
"uncharted",
"uniregistry",
"unity",
"unsplash",
"untappd",
"ups",
"usb",
"usps",
"ussunnah",
"vaadin",
"viacoin",
"viadeo",
"viadeo-square",
"viber",
"vimeo",
"vimeo-square",
"vimeo-v",
"vine",
"vk",
"vnv",
"vuejs",
"watchman-monitoring",
"waze",
"weebly",
"weibo",
"weixin",
"whatsapp",
"whatsapp-square",
"whmcs",
"wikipedia-w",
"windows",
"wix",
"wizards-of-the-coast",
"wodu",
"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

@@ -28,14 +28,6 @@ register_s3_method <- function(pkg, generic, class, fun = NULL) {
}
register_upgrade_message <- function(pkg, version) {
# Is an out-dated version of this package installed?
needs_upgrade <- function() {
if (system.file(package = pkg) == "")
return(FALSE)
if (utils::packageVersion(pkg) >= version)
return(FALSE)
TRUE
}
msg <- sprintf(
"This version of Shiny is designed to work with '%s' >= %s.
@@ -43,7 +35,7 @@ register_upgrade_message <- function(pkg, version) {
pkg, version, pkg
)
if (pkg %in% loadedNamespaces() && needs_upgrade()) {
if (pkg %in% loadedNamespaces() && !is_available(pkg, version)) {
packageStartupMessage(msg)
}
@@ -53,7 +45,7 @@ register_upgrade_message <- function(pkg, version) {
setHook(
packageEvent(pkg, "onLoad"),
function(...) {
if (needs_upgrade()) packageStartupMessage(msg)
if (!is_available(pkg, version)) packageStartupMessage(msg)
}
)
}
@@ -64,6 +56,10 @@ register_upgrade_message <- function(pkg, version) {
# the private seed during load.
withPrivateSeed(set.seed(NULL))
# Create this at the top level, but since the object is from a different
# package, we don't want to bake it into the built binary package.
restoreCtxStack <<- fastmap::faststack()
# Make sure these methods are available to knitr if shiny is loaded but not
# attached.
register_s3_method("knitr", "knit_print", "reactive")

View File

@@ -1,31 +1,20 @@
is_installed <- function(package, version) {
installedVersion <- tryCatch(utils::packageVersion(package), error = function(e) NA)
!is.na(installedVersion) && installedVersion >= version
}
# 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, location) {
check_suggested <- function(package, version = NULL) {
if (is_installed(package, version)) {
if (is_available(package, version)) {
return()
}
missing_location <- missing(location)
msg <- paste0(
sQuote(package),
if (is.na(version)) "" else paste0("(>= ", version, ")"),
" must be installed for this functionality.",
if (!missing_location)
paste0(
"\nPlease install the missing package: \n",
" source(\"https://install-github.me/", location, "\")"
)
if (is.na(version %||% NA)) "" else paste0("(>= ", version, ")"),
" must be installed for this functionality."
)
if (interactive() && missing_location) {
if (interactive()) {
message(msg, "\nWould you like to install it?")
if (utils::menu(c("Yes", "No")) == 1) {
return(utils::install.packages(package))
@@ -98,7 +87,8 @@ reactlog <- function() {
}
#' @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()
@@ -108,7 +98,8 @@ reactlogShow <- function(time = TRUE) {
#' @export
# legacy purposes
showReactLog <- function(time = TRUE) {
shinyDeprecated(new = "`reactlogShow`", version = "1.2.0")
shinyDeprecated("1.2.0", "showReactLog()", "reactlogShow()")
reactlogShow(time = time)
}
#' @describeIn reactlog Resets the entire reactlog stack. Useful for debugging and removing all prior reactive history.
@@ -190,10 +181,10 @@ RLog <- R6Class(
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)
@@ -221,7 +212,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
@@ -569,5 +560,4 @@ MessageLogger = R6Class(
)
)
#' @include stack.R
rLog <- RLog$new("shiny.reactlog", "shiny.reactlog.console")

View File

@@ -49,6 +49,12 @@ processDeps <- function(tags, session) {
)
names(dependencies) <- NULL
# If ui is a tagFunction() (e.g., insertTab() et al),
# then doRenderTags() won't work...
if (inherits(ui, "shiny.tag.function")) {
ui <- renderTags(ui)$html
}
list(
html = doRenderTags(ui),
deps = dependencies

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,16 +1,27 @@
#' 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 = NULL, clip = TRUE) {
#' @keywords internal
clickOpts <- function(id, clip = TRUE) {
if (is.null(id))
stop("id must not be NULL")
@@ -21,22 +32,9 @@ clickOpts <- function(id = NULL, 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
dblclickOpts <- function(id = NULL, clip = TRUE, delay = 400) {
#' @rdname clickOpts
dblclickOpts <- function(id, clip = TRUE, delay = 400) {
if (is.null(id))
stop("id must not be NULL")
@@ -47,29 +45,12 @@ dblclickOpts <- function(id = NULL, 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
hoverOpts <- function(id = NULL, delay = 300,
#' @rdname clickOpts
hoverOpts <- function(id, delay = 300,
delayType = c("debounce", "throttle"), clip = TRUE,
nullOutside = TRUE) {
if (is.null(id))
@@ -95,8 +76,12 @@ hoverOpts <- function(id = NULL, 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,8 +101,9 @@ hoverOpts <- function(id = NULL, 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 = NULL, fill = "#9cf", stroke = "#036",
brushOpts <- function(id, fill = "#9cf", stroke = "#036",
opacity = 0.25, delay = 300,
delayType = c("debounce", "throttle"), clip = TRUE,
direction = c("xy", "x", "y"),
@@ -125,6 +111,13 @@ brushOpts <- function(id = NULL, 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 +130,13 @@ brushOpts <- function(id = NULL, 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

@@ -1,59 +1,76 @@
#' Find rows of data that are selected by a brush
#' Find rows of data selected on an interactive plot.
#'
#' This function returns rows from a data frame which are under a brush used
#' with [plotOutput()].
#' @description
#' `brushedPoints()` returns rows from a data frame which are under a brush.
#' `nearPoints()` returns rows from a data frame which are near a click, hover,
#' or double-click. Alternatively, set `allRows = TRUE` to return all rows from
#' the input data with an additional column `selected_` that indicates which
#' rows of the would be selected.
#'
#' It is also possible for this function to return all rows from the input data
#' frame, but with an additional column `selected_`, which indicates which
#' rows of the input data frame are selected by the brush (`TRUE` for
#' selected, `FALSE` for not-selected). This is enabled by setting
#' `allRows=TRUE` option.
#' @section ggplot2:
#' For plots created with ggplot2, it is not necessary to specify the
#' column names to `xvar`, `yvar`, `panelvar1`, and `panelvar2` as that
#' information can be automatically derived from the plot specification.
#'
#' The `xvar`, `yvar`, `panelvar1`, and `panelvar2`
#' arguments specify which columns in the data correspond to the x variable, y
#' variable, and panel variables of the plot. For example, if your plot is
#' `plot(x=cars$speed, y=cars$dist)`, and your brush is named
#' `"cars_brush"`, then you would use `brushedPoints(cars,
#' input$cars_brush, "speed", "dist")`.
#'
#' For plots created with ggplot2, it should not be necessary to specify the
#' column names; that information will already be contained in the brush,
#' provided that variables are in the original data, and not computed. For
#' example, with `ggplot(cars, aes(x=speed, y=dist)) + geom_point()`, you
#' could use `brushedPoints(cars, input$cars_brush)`. If, however, you use
#' a computed column, like `ggplot(cars, aes(x=speed/2, y=dist)) +
#' geom_point()`, then it will not be able to automatically extract column names
#' and filter on them. If you want to use this function to filter data, it is
#' recommended that you not use computed columns; instead, modify the data
#' Note, however, that this will not work if you use a computed column, like
#' `aes(speed/2, dist))`. Instead, we recommend that you modify the data
#' first, and then make the plot with "raw" columns in the modified data.
#'
#' If a specified x or y column is a factor, then it will be coerced to an
#' integer vector. If it is a character vector, then it will be coerced to a
#' factor and then integer vector. This means that the brush will be considered
#' to cover a given character/factor value when it covers the center value.
#' @section Brushing:
#' If x or y column is a factor, then it will be coerced to an integer vector.
#' If it is a character vector, then it will be coerced to a factor and then
#' integer vector. This means that the brush will be considered to cover a
#' given character/factor value when it covers the center value.
#'
#' If the brush is operating in just the x or y directions (e.g., with
#' `brushOpts(direction = "x")`, then this function will filter out points
#' using just the x or y variable, whichever is appropriate.
#'
#' @param brush The data from a brush, such as `input$plot_brush`.
#' @returns
#' A data frame based on `df`, containing the observations selected by the
#' brush or near the click event. For `nearPoints()`, the rows will be sorted
#' by distance to the event.
#'
#' If `allRows = TRUE`, then all rows will returned, along with a new
#' `selected_` column that indicates whether or not the point was selected.
#' The output from `nearPoints()` will no longer be sorted, but you can
#' set `addDist = TRUE` to get an additional column that gives the pixel
#' distance to the pointer.
#'
#' @param df A data frame from which to select rows.
#' @param xvar,yvar A string with the name of the variable on the x or y axis.
#' This must also be the name of a column in `df`. If absent, then this
#' function will try to infer the variable from the brush (only works for
#' ggplot2).
#' @param panelvar1,panelvar2 Each of these is a string with the name of a panel
#' variable. For example, if with ggplot2, you facet on a variable called
#' `cyl`, then you can use `"cyl"` here. However, specifying the
#' panel variable should not be necessary with ggplot2; Shiny should be able
#' to auto-detect the panel variable.
#' @param brush,coordinfo The data from a brush or click/dblclick/hover event
#' e.g. `input$plot_brush`, `input$plot_click`.
#' @param xvar,yvar A string giving the name of the variable on the x or y axis.
#' These are only required for base graphics, and must be the name of
#' a column in `df`.
#' @param panelvar1,panelvar2 A string giving the name of a panel variable.
#' For expert use only; in most cases these will be automatically
#' derived from the ggplot2 spec.
#' @param allRows If `FALSE` (the default) return a data frame containing
#' the selected rows. If `TRUE`, the input data frame will have a new
#' column, `selected_`, which indicates whether the row was inside the
#' brush (`TRUE`) or outside the brush (`FALSE`).
#'
#' column, `selected_`, which indicates whether the row was selected or not.
#' @param threshold A maximum distance (in pixels) to the pointer location.
#' Rows in the data frame will be selected if the distance to the pointer is
#' less than `threshold`.
#' @param maxpoints Maximum number of rows to return. If `NULL` (the default),
#' will return all rows within the threshold distance.
#' @param addDist If TRUE, add a column named `dist_` that contains the
#' distance from the coordinate to the point, in pixels. When no pointer
#' event has yet occurred, the value of `dist_` will be `NA`.
#' @seealso [plotOutput()] for example usage.
#' @export
#' @examples
#' \dontrun{
#' # Note that in practice, these examples would need to go in reactives
#' # or observers.
#'
#' # This would select all points within 5 pixels of the click
#' nearPoints(mtcars, input$plot_click)
#'
#' # Select just the nearest point within 10 pixels of the click
#' nearPoints(mtcars, input$plot_click, threshold = 10, maxpoints = 1)
#'
#' }
brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
panelvar1 = NULL, panelvar2 = NULL,
allRows = FALSE) {
@@ -75,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))
@@ -191,56 +218,8 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
# $ direction: chr "y"
#'Find rows of data that are near a click/hover/double-click
#'
#'This function returns rows from a data frame which are near a click, hover, or
#'double-click, when used with [plotOutput()]. The rows will be sorted
#'by their distance to the mouse event.
#'
#'It is also possible for this function to return all rows from the input data
#'frame, but with an additional column `selected_`, which indicates which
#'rows of the input data frame are selected by the brush (`TRUE` for
#'selected, `FALSE` for not-selected). This is enabled by setting
#'`allRows=TRUE` option. If this is used, the resulting data frame will not
#'be sorted by distance to the mouse event.
#'
#'The `xvar`, `yvar`, `panelvar1`, and `panelvar2` arguments
#'specify which columns in the data correspond to the x variable, y variable,
#'and panel variables of the plot. For example, if your plot is
#'`plot(x=cars$speed, y=cars$dist)`, and your click variable is named
#'`"cars_click"`, then you would use `nearPoints(cars,
#'input$cars_brush, "speed", "dist")`.
#'
#'@inheritParams brushedPoints
#'@param coordinfo The data from a mouse event, such as `input$plot_click`.
#'@param threshold A maxmimum distance to the click point; rows in the data
#' frame where the distance to the click is less than `threshold` will be
#' returned.
#'@param maxpoints Maximum number of rows to return. If NULL (the default),
#' return all rows that are within the threshold distance.
#'@param addDist If TRUE, add a column named `dist_` that contains the
#' distance from the coordinate to the point, in pixels. When no mouse event
#' has yet occured, the value of `dist_` will be `NA`.
#'@param allRows If `FALSE` (the default) return a data frame containing
#' the selected rows. If `TRUE`, the input data frame will have a new
#' column, `selected_`, which indicates whether the row was inside the
#' selected by the mouse event (`TRUE`) or not (`FALSE`).
#'
#'@seealso [plotOutput()] for more examples.
#'
#' @examples
#' \dontrun{
#' # Note that in practice, these examples would need to go in reactives
#' # or observers.
#'
#' # This would select all points within 5 pixels of the click
#' nearPoints(mtcars, input$plot_click)
#'
#' # Select just the nearest point within 10 pixels of the click
#' nearPoints(mtcars, input$plot_click, threshold = 10, maxpoints = 1)
#'
#' }
#'@export
#' @export
#' @rdname brushedPoints
nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
panelvar1 = NULL, panelvar2 = NULL,
threshold = 5, maxpoints = NULL, addDist = FALSE,
@@ -261,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")

View File

@@ -1,17 +1,46 @@
startPNG <- function(filename, width, height, res, ...) {
# If quartz is available, use png() (which will default to quartz).
# Otherwise, if the Cairo package is installed, use CairoPNG().
# Finally, if neither quartz nor Cairo, use png().
if (capabilities("aqua")) {
# 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') %||% FALSE) && is_available("ragg")) {
pngfun <- ragg::agg_png
} else if (capabilities("aqua")) {
# i.e., png(type = 'quartz')
pngfun <- grDevices::png
} else if ((getOption('shiny.usecairo') %OR% TRUE) &&
nchar(system.file(package = "Cairo"))) {
} else if ((getOption('shiny.usecairo') %||% TRUE) && is_available("Cairo")) {
pngfun <- Cairo::CairoPNG
} else {
# i.e., png(type = 'cairo')
pngfun <- grDevices::png
}
pngfun(filename=filename, width=width, height=height, res=res, ...)
args <- rlang::list2(filename=filename, width=width, height=height, res=res, ...)
# 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")) {
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"]]()
}
}
# Handle both bg and background device arg
# https://github.com/r-lib/ragg/issues/35
fmls <- names(formals(pngfun))
if (("background" %in% fmls) && (!"bg" %in% fmls)) {
if (is.null(args$background)) {
args$background <- args$bg
}
args$bg <- NULL
}
do.call(pngfun, args)
# Call plot.new() so that even if no plotting operations are performed at
# least we have a blank background. N.B. we need to set the margin to 0
# temporarily before plot.new() because when the plot size is small (e.g.
@@ -65,7 +94,6 @@ plotPNG <- function(func, filename=tempfile(fileext='.png'),
filename
}
#' @importFrom grDevices dev.set dev.cur
createGraphicsDevicePromiseDomain <- function(which = dev.cur()) {
force(which)

View File

@@ -16,7 +16,7 @@
#'
#' ui <- fluidPage(
#' sliderInput("obs", "Number of observations", 0, 1000, 500),
#' actionButton("goButton", "Go!"),
#' actionButton("goButton", "Go!", class = "btn-success"),
#' plotOutput("distPlot")
#' )
#'
@@ -36,6 +36,10 @@
#'
#' }
#'
#' ## Example of adding extra class values
#' actionButton("largeButton", "Large Primary Button", class = "btn-primary btn-lg")
#' actionLink("infoLink", "Information Link", class = "btn-info")
#'
#' @seealso [observeEvent()] and [eventReactive()]
#'
#' @section Server value:
@@ -50,7 +54,7 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
value <- restoreInput(id = inputId, default = NULL)
tags$button(id=inputId,
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
style = css(width = validateCssUnit(width)),
type="button",
class="btn btn-default action-button",
`data-val` = value,

View File

@@ -36,7 +36,7 @@ checkboxInput <- function(inputId, label, value = FALSE, width = NULL) {
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,11 +105,15 @@ 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",
class = "form-control",
# `aria-labelledby` attribute is required for accessibility to avoid doubled labels (#2951).
`aria-labelledby` = paste0(inputId, "-label"),
# title attribute is announced for screen readers for date format.
title = paste("Date format:", format),
`data-date-language` = language,
`data-date-week-start` = weekstart,
`data-date-format` = format,
@@ -124,19 +128,49 @@ 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>"
)
datePickerVersion <- "1.9.0"
datePickerDependency <- function(theme) {
list(
htmlDependency(
name = "bootstrap-datepicker-js",
version = datePickerVersion,
src = c(href = "shared/datepicker"),
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)
)
}
datePickerCSS <- function(theme) {
if (!is_bs_theme(theme)) {
return(htmlDependency(
name = "bootstrap-datepicker-css",
version = datePickerVersion,
src = c(href = "shared/datepicker"),
stylesheet = "css/bootstrap-datepicker3.min.css"
))
}
scss_file <- system.file(package = "shiny", "www/shared/datepicker/scss/build3.scss")
bslib::bs_dependency(
input = sass::sass_file(scss_file),
theme = theme,
name = "bootstrap-datepicker",
version = datePickerVersion,
cache_key_extra = shinyPackageVersion()
)
}

View File

@@ -92,14 +92,18 @@ 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
div(class = "input-daterange input-group",
div(class = "input-daterange input-group input-group-sm",
tags$input(
class = "input-sm form-control",
class = "form-control",
type = "text",
# `aria-labelledby` attribute is required for accessibility to avoid doubled labels (#2951).
`aria-labelledby` = paste0(inputId, "-label"),
# title attribute is announced for screen readers for date format.
title = paste("Date format:", format),
`data-date-language` = language,
`data-date-week-start` = weekstart,
`data-date-format` = format,
@@ -109,10 +113,19 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
`data-initial-date` = start,
`data-date-autoclose` = if (autoclose) "true" else "false"
),
span(class = "input-group-addon", separator),
# input-group-prepend and input-group-append are for bootstrap 4 forward compat
span(class = "input-group-addon input-group-prepend input-group-append",
span(class = "input-group-text",
separator
)
),
tags$input(
class = "input-sm form-control",
class = "form-control",
type = "text",
# `aria-labelledby` attribute is required for accessibility to avoid doubled labels (#2951).
`aria-labelledby` = paste0(inputId, "-label"),
# title attribute is announced for screen readers for date format.
title = paste("Date format:", format),
`data-date-language` = language,
`data-date-week-start` = weekstart,
`data-date-format` = format,
@@ -124,6 +137,6 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
)
)
),
datePickerDependency
datePickerDependency()
)
}

View File

@@ -11,8 +11,15 @@
#' @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 MIME types; gives the browser a hint of
#' what kind of files the server is expecting.
#' @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.
#' @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.
@@ -24,13 +31,7 @@
#' ui <- fluidPage(
#' sidebarLayout(
#' sidebarPanel(
#' fileInput("file1", "Choose CSV File",
#' accept = c(
#' "text/csv",
#' "text/comma-separated-values,text/plain",
#' ".csv")
#' ),
#' tags$hr(),
#' fileInput("file1", "Choose CSV File", accept = ".csv"),
#' checkboxInput("header", "Header", TRUE)
#' ),
#' mainPanel(
@@ -41,17 +42,13 @@
#'
#' server <- function(input, output) {
#' output$contents <- renderTable({
#' # input$file1 will be NULL initially. After the user selects
#' # and uploads a file, it will be a data frame with 'name',
#' # 'size', 'type', and 'datapath' columns. The 'datapath'
#' # column will contain the local filenames where the data can
#' # be found.
#' inFile <- input$file1
#' file <- input$file1
#' ext <- tools::file_ext(file$datapath)
#'
#' if (is.null(inFile))
#' return(NULL)
#' req(file)
#' validate(need(ext == "csv", "Please upload a csv file"))
#'
#' read.csv(inFile$datapath, header = input$header)
#' read.csv(file$datapath, header = input$header)
#' })
#' }
#'
@@ -94,7 +91,8 @@ fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
id = inputId,
name = inputId,
type = "file",
style = "display: none;",
# 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/
style = "position: absolute !important; top: -99999px !important; left: -99999px !important;",
`data-restore` = restoredValue
)
@@ -105,11 +103,12 @@ fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
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",
tags$label(class = "input-group-btn",
# input-group-prepend is for bootstrap 4 compat
tags$label(class = "input-group-btn input-group-prepend",
span(class = "btn btn-default btn-file",
buttonLabel,
inputTag
@@ -122,7 +121,7 @@ fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
tags$div(
id=paste(inputId, "_progress", sep=""),
class="progress progress-striped active shiny-file-input-progress",
class="progress active shiny-file-input-progress",
tags$div(class="progress-bar")
)
)

View File

@@ -45,7 +45,7 @@ numericInput <- function(inputId, label, value, min = NA, max = NA, step = NA,
inputTag$attribs$step = step
div(class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
inputTag
)

View File

@@ -33,7 +33,7 @@
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), ";"),
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
tags$input(id = inputId, type="password", class="form-control", value=value,
placeholder = placeholder)

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

@@ -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
@@ -100,7 +108,7 @@ selectInput <- function(inputId, label, choices, selected = NULL,
id = inputId,
class = if (!selectize) "form-control",
size = size,
selectOptions(choices, selected)
selectOptions(choices, selected, inputId, selectize)
)
if (multiple)
selectTag$attribs$multiple <- "multiple"
@@ -108,7 +116,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 +133,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 {
@@ -183,24 +197,30 @@ 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'))
}
res <- checkAsIs(options)
selectizeDep <- htmlDependency(
"selectize", "0.11.2", c(href = "shared/selectize"),
stylesheet = "css/selectize.bootstrap3.css",
head = format(tagList(
HTML('<!--[if lt IE 9]>'),
tags$script(src = 'shared/selectize/js/es5-shim.min.js'),
HTML('<![endif]-->'),
tags$script(src = 'shared/selectize/js/selectize.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 <- c(
deps,
list(htmlDependency(
'jqueryui', '1.12.1',
c(href = 'shared/jqueryui'),
script = 'jquery-ui.min.js'
))
)
}
# Insert script on same level as <select> tag
@@ -210,18 +230,64 @@ 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) {
selectizeVersion <- "0.12.4"
if (!is_bs_theme(theme)) {
return(selectizeStaticDependency(selectizeVersion))
}
selectizeDir <- system.file(package = "shiny", "www/shared/selectize/")
stylesheet <- file.path(
selectizeDir, "scss",
if ("3" %in% bslib::theme_version(theme)) {
"selectize.bootstrap3.scss"
} else {
"selectize.bootstrap4.scss"
}
)
# It'd be cleaner to ship the JS in a separate, href-based,
# HTML dependency (which we currently do for other themable widgets),
# but DT, crosstalk, and maybe other pkgs include selectize JS/CSS
# 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
script <- file.path(
selectizeDir, c("js/selectize.min.js", "accessibility/js/selectize-plugin-a11y.min.js")
)
bslib::bs_dependency(
input = sass::sass_file(stylesheet),
theme = theme,
name = "selectize",
version = selectizeVersion,
cache_key_extra = shinyPackageVersion(),
.dep_args = list(script = script)
)
}
selectizeStaticDependency <- function(version) {
htmlDependency(
"selectize", version,
src = c(href = "shared/selectize"),
stylesheet = "css/selectize.bootstrap3.css",
script = c(
"js/selectize.min.js",
"accessibility/js/selectize-plugin-a11y.min.js"
)
)
}
#' Select variables from a data frame

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,64 @@ 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())
}
ionRangeSliderVersion <- "2.3.1"
ionRangeSliderDependency <- function() {
list(
# ion.rangeSlider also needs normalize.css, which is already included in Bootstrap.
htmlDependency(
"ionrangeslider-javascript", ionRangeSliderVersion,
src = c(href = "shared/ionrangeslider"),
script = "js/ion.rangeSlider.min.js"
),
htmlDependency("strftime", "0.9.2", c(href="shared/strftime"),
htmlDependency(
"strftime", "0.9.2",
src = c(href = "shared/strftime"),
script = "strftime-min.js"
),
bslib::bs_dependency_defer(ionRangeSliderDependencyCSS)
)
}
ionRangeSliderDependencyCSS <- function(theme) {
if (!is_bs_theme(theme)) {
return(htmlDependency(
"ionrangeslider-css",
ionRangeSliderVersion,
src = c(href = "shared/ionrangeslider"),
stylesheet = "css/ion.rangeSlider.css"
))
}
# Remap some variable names for ionRangeSlider's scss
sass_input <- list(
list(
# The bootswatch materia theme sets $input-bg: transparent;
# which is an issue for the slider's handle(s) (#3130)
bg = "if(alpha($input-bg)==0, $body-bg, $input-bg)",
fg = sprintf(
"if(alpha($input-color)==0, $%s, $input-color)",
if ("3" %in% bslib::theme_version(theme)) "text-color" else "body-color"
),
accent = "$component-active-bg",
`font-family` = "$font-family-base"
),
sass::sass_file(
system.file(package = "shiny", "www/shared/ionrangeslider/scss/shiny.scss")
)
)
attachDependencies(sliderTag, dep)
bslib::bs_dependency(
input = sass_input,
theme = theme,
name = "ionRangeSlider",
version = ionRangeSliderVersion,
cache_key_extra = shinyPackageVersion()
)
}
hasDecimals <- function(value) {
@@ -226,7 +262,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 +288,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
@@ -58,7 +58,7 @@ submitButton <- function(text = "Apply Changes", icon = NULL, width = NULL) {
tags$button(
type="submit",
class="btn btn-primary",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
style = css(width = validateCssUnit(width)),
list(icon, text)
)
)

View File

@@ -40,7 +40,7 @@ textInput <- function(inputId, label, value = "", width = NULL,
value <- restoreInput(id = inputId, default = value)
div(class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
tags$input(id = inputId, type="text", class="form-control", value=value,
placeholder = placeholder)

View File

@@ -50,18 +50,16 @@ 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, ";")
style <- css(
# The width is specified on the parent div.
width = if (!is.null(width)) "width: 100%;",
height = validateCssUnit(height),
resize = resize
)
# 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",
shinyInputLabel(inputId, label),
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
tags$textarea(
id = inputId,
class = "form-control",

View File

@@ -3,6 +3,8 @@ shinyInputLabel <- function(inputId, label = NULL) {
label,
class = "control-label",
class = if (is.null(label)) "shiny-label-null",
# `id` attribute is required for `aria-labelledby` used by screen readers:
id = paste0(inputId, "-label"),
`for` = inputId
)
}

View File

@@ -1,55 +1,54 @@
#' Insert UI objects
#' Insert and remove UI objects
#'
#' Insert a UI object into the app.
#'
#' This function allows you to dynamically add an arbitrarily large UI
#' object into your app, whenever you want, as many times as you want.
#' Unlike [renderUI()], the UI generated with `insertUI`
#' is not updatable as a whole: once it's created, it stays there. Each
#' new call to `insertUI` creates more UI objects, in addition to
#' These functions allow you to dynamically add and remove arbirary 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
#' new call to `insertUI()` creates more UI objects, in addition to
#' the ones already there (all independent from one another). To
#' update a part of the UI (ex: an input object), you must use the
#' appropriate `render` function or a customized `reactive`
#' function. To remove any part of your UI, use [removeUI()].
#' function.
#'
#' @param selector A string that is accepted by jQuery's selector (i.e. the
#' string `s` to be placed in a `$(s)` jQuery call). This selector
#' will determine the element(s) relative to which you want to insert your
#' UI object.
#' 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
#' through a jQuery selector can be removed through this function.
#'
#' @param selector A string that is accepted by jQuery's selector
#' (i.e. the string `s` to be placed in a `$(s)` jQuery call).
#'
#' For `insertUI()` this determines the element(s) relative to which you
#' want to insert your UI object. For `removeUI()` this determine the
#' element(s) to be removed. If you want to remove a Shiny input or output,
#' note that many of these are wrapped in `<div>`s, so you may need to use a
#' somewhat complex selector --- see the Examples below. (Alternatively, you
#' could also wrap the inputs/outputs that you want to be able to remove
#' easily in a `<div>` with an id.)
#' @param where Where your UI object should go relative to the selector:
#' \describe{
#' \item{`beforeBegin`}{Before the selector element itself}
#' \item{`afterBegin`}{Just inside the selector element, before its
#' first child}
#' \item{`beforeEnd`}{Just inside the selector element, after its
#' last child (default)}
#' \item{`afterEnd`}{After the selector element itself}
#' }
#' Adapted from
#' [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML).
#'
#' \describe{
#' \item{`beforeBegin`}{Before the selector element itself}
#' \item{`afterBegin`}{Just inside the selector element, before its
#' first child}
#' \item{`beforeEnd`}{Just inside the selector element, after its
#' last child (default)}
#' \item{`afterEnd`}{After the selector element itself}
#' }
#' Adapted from <https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML>.
#' @param ui The UI object you want to insert. This can be anything that
#' you usually put inside your apps's `ui` function. If you're inserting
#' multiple elements in one call, make sure to wrap them in either a
#' `tagList()` or a `tags$div()` (the latter option has the
#' advantage that you can give it an `id` to make it easier to
#' reference or remove it later on). If you want to insert raw html, use
#' `ui = HTML()`.
#'
#' you usually put inside your apps's `ui` function. If you're inserting
#' multiple elements in one call, make sure to wrap them in either a
#' `tagList()` or a `tags$div()` (the latter option has the
#' advantage that you can give it an `id` to make it easier to
#' reference or remove it later on). If you want to insert raw html, use
#' `ui = HTML()`.
#' @param multiple In case your selector matches more than one element,
#' `multiple` determines whether Shiny should insert the UI object
#' relative to all matched elements or just relative to the first
#' matched element (default).
#'
#' @param immediate Whether the UI object should be immediately inserted into
#' the app when you call `insertUI`, or whether Shiny should wait until
#' all outputs have been updated and all observers have been run (default).
#'
#' @param session The shiny session within which to call `insertUI`.
#'
#' @seealso [removeUI()]
#'
#' `multiple` determines whether Shiny should insert the UI object
#' relative to all matched elements or just relative to the first
#' matched element (default).
#' @param immediate Whether the UI object should be immediately inserted
#' or removed, or whether Shiny should wait until all outputs have been
#' updated and all observers have been run (default).
#' @param session The shiny session. Advanced use only.
#' @examples
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
@@ -73,6 +72,26 @@
#' # Complete app with UI and server components
#' shinyApp(ui, server)
#' }
#'
#' if (interactive()) {
#' # Define UI
#' ui <- fluidPage(
#' actionButton("rmv", "Remove UI"),
#' textInput("txt", "This is no longer useful")
#' )
#'
#' # Server logic
#' server <- function(input, output, session) {
#' observeEvent(input$rmv, {
#' removeUI(
#' selector = "div:has(> #txt)"
#' )
#' })
#' }
#'
#' # Complete app with UI and server components
#' shinyApp(ui, server)
#' }
#' @export
insertUI <- function(selector,
where = c("beforeBegin", "afterBegin", "beforeEnd", "afterEnd"),
@@ -100,60 +119,7 @@ insertUI <- function(selector,
}
#' Remove UI objects
#'
#' Remove a UI object from the app.
#'
#' This function allows you to remove any part of your UI. Once `removeUI`
#' is executed on some element, it is gone forever.
#'
#' While it may be a particularly useful pattern to pair this with
#' [insertUI()] (to remove some UI you had previously inserted),
#' there is no restriction on what you can use `removeUI` 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 (i.e. the
#' string `s` to be placed in a `$(s)` jQuery call). This selector
#' will determine the element(s) to be removed. If you want to remove a
#' Shiny input or output, note that many of these are wrapped in `div`s,
#' so you may need to use a somewhat complex selector --- see the Examples below.
#' (Alternatively, you could also wrap the inputs/outputs that you want to be
#' able to remove easily in a `div` with an id.)
#'
#' @param multiple In case your selector matches more than one element,
#' `multiple` determines whether Shiny should remove all the matched
#' elements or just the first matched element (default).
#'
#' @param immediate Whether the element(s) should be immediately removed from
#' the app when you call `removeUI`, or whether Shiny should wait until
#' all outputs have been updated and all observers have been run (default).
#'
#' @param session The shiny session within which to call `removeUI`.
#'
#' @seealso [insertUI()]
#'
#' @examples
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#' # Define UI
#' ui <- fluidPage(
#' actionButton("rmv", "Remove UI"),
#' textInput("txt", "This is no longer useful")
#' )
#'
#' # Server logic
#' server <- function(input, output, session) {
#' observeEvent(input$rmv, {
#' removeUI(
#' selector = "div:has(> #txt)"
#' )
#' })
#' }
#'
#' # Complete app with UI and server components
#' shinyApp(ui, server)
#' }
#' @rdname insertUI
#' @export
removeUI <- function(selector,
multiple = FALSE,

80
R/knitr.R Normal file
View File

@@ -0,0 +1,80 @@
#' 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 %||% 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)
}

15
R/map.R
View File

@@ -1,18 +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',

View File

@@ -14,7 +14,26 @@
# returns `NULL`, or an `httpResponse`.
#
## ------------------------------------------------------------------------
httpResponse <- function(status = 200,
#' Create an HTTP response object
#'
#' @param status HTTP status code for the response.
#' @param content_type The value for the `Content-Type` header.
#' @param content The body of the response, given as a single-element character
#' vector (will be encoded as UTF-8) or a raw vector.
#' @param headers A named list of additional headers to include. Do not include
#' `Content-Length` (as it is automatically calculated) or `Content-Type` (the
#' `content_type` argument is used instead).
#'
#' @examples
#' httpResponse(status = 405L,
#' content_type = "text/plain",
#' content = "The requested method was not allowed"
#' )
#'
#' @keywords internal
#' @export
httpResponse <- function(status = 200L,
content_type = "text/html; charset=UTF-8",
content = "",
headers = list()) {
@@ -290,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)
@@ -311,16 +330,32 @@ HandlerManager <- R6Class("HandlerManager",
},
call = .httpServer(
function (req) {
withCallingHandlers(withLogErrors(handlers$invoke(req)),
error = function(cond) {
sanitizeErrors <- getOption('shiny.sanitize.errors', FALSE)
if (inherits(cond, 'shiny.custom.error') || !sanitizeErrors) {
stop(cond$message, call. = FALSE)
} else {
stop(paste("An error has occurred. Check your logs or",
"contact the app author for clarification."),
call. = FALSE)
hybrid_chain(
hybrid_chain(
withCallingHandlers(withLogErrors(handlers$invoke(req)),
error = function(cond) {
sanitizeErrors <- getOption('shiny.sanitize.errors', FALSE)
if (inherits(cond, 'shiny.custom.error') || !sanitizeErrors) {
stop(cond$message, call. = FALSE)
} else {
stop(paste("An error has occurred. Check your logs or",
"contact the app author for clarification."),
call. = FALSE)
}
}
),
catch = function(err) {
httpResponse(status = 500L,
content_type = "text/html; charset=UTF-8",
content = as.character(htmltools::htmlTemplate(
system.file("template", "error.html", package = "shiny"),
message = conditionMessage(err)
))
)
}
),
function(resp) {
maybeInjectAutoreload(resp)
}
)
},
@@ -390,6 +425,22 @@ HandlerManager <- R6Class("HandlerManager",
)
)
maybeInjectAutoreload <- function(resp) {
if (get_devmode_option("shiny.autoreload", FALSE) &&
isTRUE(grepl("^text/html($|;)", resp$content_type)) &&
is.character(resp$content)) {
resp$content <- gsub(
"</head>",
"<script src=\"shared/shiny-autoreload.js\"></script>\n</head>",
resp$content,
fixed = TRUE
)
}
resp
}
# Safely get the Content-Length of a Rook response, or NULL if the length cannot
# be determined for whatever reason (probably malformed response$content).
# If deleteOwnedContent is TRUE, then the function should delete response

719
R/mock-session.R Normal file
View File

@@ -0,0 +1,719 @@
# Promise helpers taken from:
# https://github.com/rstudio/promises/blob/master/tests/testthat/common.R
# Block until all pending later tasks have executed
wait_for_it <- function() {
while (!later::loop_empty()) {
later::run_now(0.1)
}
}
# Block until the promise is resolved/rejected. If resolved, return the value.
# If rejected, throw (yes throw, not return) the error.
extract <- function(promise) {
promise_value <- NULL
error <- NULL
promise %...>%
(function(value) promise_value <<- value) %...!%
(function(reason) error <<- reason)
wait_for_it()
if (!is.null(error))
stop(error)
else
promise_value
}
# TODO: is there a way to get this behavior without exporting these functions? R6?
# TODO: clientData is documented as a reactiveValues, which this is not. Is it possible that
# users are currently assigning into clientData? That would not work as expected here.
#' @noRd
#' @export
`$.mockclientdata` <- function(x, name) {
if (name == "pixelratio") { return(1) }
if (name == "url_protocol") { return("http:") }
if (name == "url_hostname") { return("mocksession") }
if (name == "url_port") { return(1234) }
if (name == "url_pathname") { return("/mockpath") }
if (name == "url_hash") { return("#mockhash") }
if (name == "url_hash_initial") { return("#mockhash") }
if (name == "url_search") { return("?mocksearch=1") }
clientRE <- "^output_(.+)_([^_]+)$"
if(grepl(clientRE, name)) {
# TODO: use proper regex group matching here instead of redundantly parsing
el <- sub(clientRE, "\\1", name)
att <- sub(clientRE, "\\2", name)
if (att == "width") {
return(600)
} else if (att == "height") {
return(400)
} else if (att == "hidden") {
return(FALSE)
}
}
warning("Unexpected clientdata attribute accessed: ", name)
return(NULL)
}
#' @noRd
#' @export
`[[.mockclientdata` <- `$.mockclientdata`
#' @noRd
#' @export
`[.mockclientdata` <- function(values, name) {
stop("Single-bracket indexing of mockclientdata is not allowed.")
}
#' @noRd
mapNames <- function(func, vals) {
names(vals) <- vapply(names(vals), func, character(1))
vals
}
#' Returns a noop implementation of the public method `name` of ShinySession.
#' @include shiny.R
#' @noRd
makeNoop <- function(name, msg = paste0(name, " is a noop.")) {
if (!(name %in% names(ShinySession$public_methods)))
stop(name, " is not public method of ShinySession.")
impl <- ShinySession$public_methods[[name]]
body(impl) <- rlang::expr({
# Force arguments
!!lapply(formalArgs(impl), rlang::sym)
# Evade "no visible binding" note for reference to `private`
(!!as.symbol("private"))$noopWarn(!!name, !!msg)
invisible()
})
impl
}
#' Accepts a series of symbols as arguments and generates corresponding noop
#' implementations.
#' @noRd
makeWarnNoops <- function(...) {
methods <- as.character(list(...))
names(methods) <- methods
lapply(methods, makeNoop)
}
#' Returns an implementation of a ShinySession public method that signals an
#' error.
#' @include shiny.R
#' @noRd
makeError <- function(name, msg = paste0(name, " is for internal use only.")) {
if (!(name %in% names(ShinySession$public_methods)))
stop(name, " is not public method of ShinySession.")
impl <- ShinySession$public_methods[[name]]
body(impl) <- rlang::expr({
base::stop(!!msg)
})
impl
}
#' Accepts a series of named arguments. Each name corresponds to a ShinySession
#' public method that should signal an error, and each argument corresponds to
#' an error message.
#' @noRd
makeErrors <- function(...) {
errors <- rlang::list2(...)
mapply(makeError, names(errors), errors, USE.NAMES = TRUE, SIMPLIFY = FALSE)
}
#' @noRd
makeExtraMethods <- function() {
c(makeWarnNoops(
"allowReconnect",
"decrementBusyCount",
"doBookmark",
"exportTestValues",
"flushOutput",
"getBookmarkExclude",
"getTestSnapshotUrl",
"incrementBusyCount",
"manageHiddenOutputs",
"manageInputs",
"onBookmark",
"onBookmarked",
"onInputReceived",
"onRestore",
"onRestored",
"outputOptions",
"reactlog",
# TODO Consider implementing this. Would require a new method like
# session$getDataObj() to access in a test expression.
"registerDataObj",
"reload",
"resetBrush",
"sendBinaryMessage",
"sendChangeTabVisibility",
"sendCustomMessage",
"sendInputMessage",
"sendInsertTab",
"sendInsertUI",
"sendModal",
"setCurrentTheme",
"getCurrentTheme",
"sendNotification",
"sendProgress",
"sendRemoveTab",
"sendRemoveUI",
"setBookmarkExclude",
"setShowcase",
"showProgress",
"updateQueryString"
), makeErrors(
`@uploadEnd` = "for internal use only",
`@uploadInit` = "for internal use only",
createBookmarkObservers = "for internal use only",
dispatch = "for internal use only",
handleRequest = "for internal use only",
requestFlush = "for internal use only",
startTiming = "for internal use only",
wsClosed = "for internal use only"
))
}
#' @description Adds generated instance methods to a MockShinySession instance.
#' Note that `lock_objects = FALSE` must be set in the call to `R6Class()`
#' that produced the generator object of the instance.
#' @param instance instance of an R6 object, generally a `MockShinySession`.
#' @param methods named list of method names to method implementation functions.
#' In our typical usage, each function is derived from a public method of
#' `ShinySession`. The environment of each implementation function is set to
#' `instance$.__enclos_env` before the method is added.
#' @noRd
addGeneratedInstanceMethods <- function(instance, methods = makeExtraMethods()) {
mapply(function(name, impl) {
environment(impl) <- instance$.__enclos_env__
instance[[name]] <- impl
}, names(methods), methods)
}
#' Mock Shiny Session
#'
#' @description An R6 class suitable for testing purposes. Simulates, to the
#' extent possible, the behavior of the `ShinySession` class. The `session`
#' parameter provided to Shiny server functions and modules is an instance of
#' a `ShinySession` in normal operation.
#'
#' Most kinds of module and server testing do not require this class be
#' instantiated manually. See instead [testServer()].
#'
#' In order to support advanced usage, instances of `MockShinySession` are
#' **unlocked** so that public methods and fields of instances may be
#' modified. For example, in order to test authentication workflows, the
#' `user` or `groups` fields may be overridden. Modified instances of
#' `MockShinySession` may then be passed explicitly as the `session` argument
#' of [testServer()].
#'
#' @include timer.R
#' @export
MockShinySession <- R6Class(
'MockShinySession',
portable = FALSE,
lock_objects = FALSE,
public = list(
#' @field env The environment associated with the session.
env = NULL,
#' @field returned The value returned by the module under test.
returned = NULL,
#' @field singletons Hardcoded as empty. Needed for rendering HTML (i.e. renderUI).
singletons = character(0),
#' @field clientData Mock client data that always returns a size for plots.
clientData = structure(list(), class="mockclientdata"),
#' @field output The shinyoutputs associated with the session.
output = NULL,
#' @field input The reactive inputs associated with the session.
input = NULL,
#' @field userData An environment initialized as empty.
userData = NULL,
#' @field progressStack A stack of progress objects.
progressStack = 'Stack',
#' @field token On a real `ShinySession`, used to identify this instance in URLs.
token = 'character',
#' @field cache The session cache object.
cache = NULL,
#' @field appcache The app cache object.
appcache = NULL,
#' @field restoreContext Part of bookmarking support in a real
#' `ShinySession` but always `NULL` for a `MockShinySession`.
restoreContext = NULL,
#' @field groups Character vector of groups associated with an authenticated
#' user. Always `NULL` for a `MockShinySesion`.
groups = NULL,
#' @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() {
private$.input <- ReactiveValues$new(dedupe = FALSE, label = "input")
private$flushCBs <- Callbacks$new()
private$flushedCBs <- Callbacks$new()
private$endedCBs <- Callbacks$new()
private$file_generators <- fastmap()
private$timer <- MockableTimerCallbacks$new()
self$progressStack <- fastmap::faststack()
self$userData <- new.env(parent=emptyenv())
# create output
out <- .createOutputWriter(self)
class(out) <- "shinyoutput"
self$output <- out
# Create a read-only copy of the inputs reactive.
self$input <- .createReactiveValues(private$.input, readonly = TRUE)
self$token <- createUniqueId(16)
# 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
# the option shiny.mocksession.warn = TRUE; see $noopWarn() for details.
addGeneratedInstanceMethods(self)
},
#' @description Define a callback to be invoked before a reactive flush
#' @param fun The function to invoke
#' @param once If `TRUE`, will only run once. Otherwise, will run every time reactives are flushed.
onFlush = function(fun, once=TRUE) {
if (!isTRUE(once)) {
return(private$flushCBs$register(fun))
} else {
dereg <- private$flushCBs$register(function() {
dereg()
fun()
})
return(dereg)
}
},
#' @description Define a callback to be invoked after a reactive flush
#' @param fun The function to invoke
#' @param once If `TRUE`, will only run once. Otherwise, will run every time reactives are flushed.
onFlushed = function(fun, once=TRUE) {
if (!isTRUE(once)) {
return(private$flushedCBs$register(fun))
} else {
dereg <- private$flushedCBs$register(function() {
dereg()
fun()
})
return(dereg)
}
},
#' @description Define a callback to be invoked when the session ends
#' @param sessionEndedCallback The callback to invoke when the session has ended.
onEnded = function(sessionEndedCallback) {
private$endedCBs$register(sessionEndedCallback)
},
#' @description Returns `FALSE` if the session has not yet been closed
isEnded = function(){ private$was_closed },
#' @description Returns `FALSE` if the session has not yet been closed
isClosed = function(){ private$was_closed },
#' @description Closes the session
close = function(){
for (output in private$output) {
output$suspend()
}
withReactiveDomain(self, {
private$endedCBs$invoke(onError = printError, ..stacktraceon = TRUE)
})
private$was_closed <- TRUE
},
#FIXME: this is wrong. Will need to be more complex.
#' @description Unsophisticated mock implementation that merely invokes
# the given callback immediately.
#' @param callback The callback to be invoked.
cycleStartAction = function(callback){ callback() },
#' @description Base64-encode the given file. Needed for image rendering.
#' @param name Not used
#' @param file The file to be encoded
#' @param contentType The content type of the base64-encoded string
fileUrl = function(name, file, contentType='application/octet-stream') {
bytes <- file.info(file)$size
if (is.na(bytes))
return(NULL)
fileData <- readBin(file, 'raw', n=bytes)
b64 <- rawToBase64(fileData)
return(paste('data:', contentType, ';base64,', b64, sep=''))
},
#' @description Sets reactive values associated with the `session$inputs`
#' object and flushes the reactives.
#' @param ... The inputs to set. These arguments are processed with
#' [rlang::list2()] and so are _[dynamic][rlang::dyn-dots]_. Input names
#' may not be duplicated.
#' @examples
#' \dontrun{
#' session$setInputs(x=1, y=2)
#' }
setInputs = function(...) {
vals <- rlang::dots_list(..., .homonyms = "error")
mapply(names(vals), vals, FUN = function(name, value) {
private$.input$set(name, value)
})
private$flush()
},
#' @description An internal method which shouldn't be used by others.
#' Schedules `callback` for execution after some number of `millis`
#' milliseconds.
#' @param millis The number of milliseconds on which to schedule a callback
#' @param callback The function to schedule.
.scheduleTask = function(millis, callback) {
id <- private$timer$schedule(millis, callback)
# Return a deregistration callback
function() {
invisible(private$timer$unschedule(id))
}
},
#' @description Simulate the passing of time by the given number of milliseconds.
#' @param millis The number of milliseconds to advance time.
elapse = function(millis) {
msLeft <- millis
while (msLeft > 0){
t <- private$timer$timeToNextEvent()
if (is.infinite(t) || t <= 0 || msLeft < t){
# Either there's no good upcoming event or we can't make it to it in the allotted time.
break
}
msLeft <- msLeft - t
private$timer$elapse(t)
# timerCallbacks must run before flushReact.
private$timer$executeElapsed()
private$flush()
}
private$timer$elapse(msLeft)
# Run again in case our callbacks resulted in a scheduled
# function that needs executing.
private$timer$executeElapsed()
private$flush()
},
#' @description An internal method which shouldn't be used by others.
#' @return Elapsed time in milliseconds.
.now = function() {
private$timer$getElapsed()
},
#' @description An internal method which shouldn't be used by others.
#' Defines an output in a way that sets private$currentOutputName
#' appropriately.
#' @param name The name of the output.
#' @param func The render definition.
#' @param label Not used.
defineOutput = function(name, func, label) {
force(name)
if (!is.null(private$outs[[name]]$obs)) {
private$outs[[name]]$obs$destroy()
}
if (is.null(func)) func <- missingOutput
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)
})
})
private$outs[[name]] <- list(obs = obs, func = func, promise = NULL)
},
#' @description An internal method which shouldn't be used by others. Forces
#' evaluation of any reactive dependencies of the output function.
#' @param name The name of the output.
#' @return The return value of the function responsible for rendering the
#' output.
getOutput = function(name) {
# Unlike the real outputs, we're going to return the last value rather than the unevaluated function
if (is.null(private$outs[[name]])) {
stop("The test referenced an output that hasn't been defined yet: output$", name)
}
if (is.null(private$outs[[name]]$promise)) {
# Means the output was defined but the observer hasn't had a chance to run
# yet. Run flushReact() now to force the observer to run.
flushReact()
if (is.null(private$outs[[name]]$promise)) {
stop("output$", name, " encountered an unexpected error resolving its promise")
}
}
# Make promise return
v <- extract(private$outs[[name]]$promise)
if (!is.null(v$err)){
stop(v$err)
} else if (private$file_generators$has(self$ns(name))) {
download <- private$file_generators$get(self$ns(name))
private$renderFile(self$ns(name), download)
} else {
v$val
}
},
#' @description Returns the given id prefixed by this namespace's id.
#' @param id The id to prefix with a namespace id.
#' @return The id with a namespace prefix.
ns = function(id) {
NS(private$nsPrefix, id)
},
#' @description Trigger a reactive flush right now.
flushReact = function(){
private$flush()
},
#' @description Create and return a namespace-specific session proxy.
#' @param namespace Character vector indicating a namespace.
#' @return A new session proxy.
makeScope = function(namespace) {
ns <- NS(namespace)
createSessionProxy(
self,
input = .createReactiveValues(private$.input, readonly = TRUE, ns = ns),
output = structure(.createOutputWriter(self, ns = ns), class = "shinyoutput"),
makeScope = function(namespace) self$makeScope(ns(namespace)),
ns = function(namespace) ns(namespace),
setInputs = function(...) {
self$setInputs(!!!mapNames(ns, rlang::dots_list(..., .homonyms = "error")))
}
)
},
#' @description Set the environment associated with a testServer() call, but
#' only if it has not previously been set. This ensures that only the
#' environment of the outermost module under test is the one retained. In
#' other words, the first assignment wins.
#' @param env The environment to retain.
#' @return The provided `env`.
setEnv = function(env) {
if (is.null(self$env)) {
stopifnot(all(c("input", "output", "session") %in% ls(env)))
self$env <- env
}
},
#' @description Set the value returned by the module call and proactively
#' flush. Note that this method may be called multiple times if modules
#' are nested. The last assignment, corresponding to an invocation of
#' setReturned() in the outermost module, wins.
#' @param value The value returned from the module
#' @return The provided `value`.
setReturned = function(value) {
self$returned <- value
value
},
#' @description Get the value returned by the module call.
#' @return The value returned by the module call
getReturned = function() self$returned,
#' @description Generate a distinct character identifier for use as a proxy
#' namespace.
#' @return A character identifier unique to the current session.
genId = function() {
private$idCounter <- private$idCounter + 1
paste0("proxy", private$idCounter)
},
#' @description Provides a way to access the root `MockShinySession` from
#' any descendant proxy.
#' @return The root `MockShinySession`.
rootScope = function() {
self
},
#' @description Called by observers when a reactive expression errors.
#' @param e An error object.
unhandledError = function(e) {
self$close()
},
#' @description Freeze a value until the flush cycle completes.
#' @param x A `ReactiveValues` object.
#' @param name The name of a reactive value within `x`.
freezeValue = function(x, name) {
if (!is.reactivevalues(x))
stop("x must be a reactivevalues object")
impl <- .subset2(x, 'impl')
key <- .subset2(x, 'ns')(name)
impl$freeze(key)
self$onFlushed(function() impl$thaw(key))
},
#' @description Registers the given callback to be invoked when the session
#' is closed (i.e. the connection to the client has been severed). The
#' return value is a function which unregisters the callback. If multiple
#' callbacks are registered, the order in which they are invoked is not
#' guaranteed.
#' @param sessionEndedCallback Function to call when the session ends.
onSessionEnded = function(sessionEndedCallback) {
self$onEnded(sessionEndedCallback)
},
#' @description Associated a downloadable file with the session.
#' @param name The un-namespaced output name to associate with the
#' downloadable file.
#' @param filename A string or function designating the name of the file.
#' @param contentType A string of the content type of the file. Not used by
#' `MockShinySession`.
#' @param content A function that takes a single argument file that is a
#' file path (string) of a nonexistent temp file, and writes the content
#' to that file path. (Reactive values and functions may be used from this
#' function.)
registerDownload = function(name, filename, contentType, content) {
private$file_generators$set(self$ns(name), list(
filename = if (is.function(filename)) filename else function() filename,
content = content
))
},
#' @description Get information about the output that is currently being
#' executed.
#' @return A list with with the `name` of the output. If no output is
#' currently being executed, this will return `NULL`.
getCurrentOutputInfo = function() {
name <- private$currentOutputName
if (is.null(name)) NULL else list(name = name)
}
),
private = list(
# @field .input Internal ReactiveValues object for normal input sent from client.
.input = NULL,
# @field flushCBs `Callbacks` called before flush.
flushCBs = NULL,
# @field flushedCBs `Callbacks` called after flush.
flushedCBs = NULL,
# @field endedCBs `Callbacks` called when session ends.
endedCBs = NULL,
# @field timer `MockableTimerCallbacks` called at particular times.
timer = NULL,
# @field was_closed Set to `TRUE` once the session is closed.
was_closed = FALSE,
# @field outs List of namespaced output names.
outs = list(),
# @field nsPrefix Prefix with which to namespace inputs and outputs.
nsPrefix = "mock-session",
# @field idCounter Incremented every time `$genId()` is called.
idCounter = 0,
# @field file_generators Map of namespaced output names to lists with
# `filename` and `output` elements, each a function. Updated by
# `$registerDownload()` and read by `$getOutput()`. Files are generated
# on demand when the output is accessed.
file_generators = NULL,
# @field currentOutputName Namespaced name of the currently executing
#' output, or `NULL` if no output is currently executing.
currentOutputName = NULL,
# @description Writes a downloadable file to disk. If the `content` function
# associated with a download handler does not write a file, an error is
# signaled. Created files are deleted upon session close.
# @param name The eamespaced output name associated with the downloadable
# file.
# @param download List with two names, `filename` and `content`. Both should
# be functions. `filename` should take no arguments and return a string.
# `content` should accept a path argument and create a file at that path.
# @return A path to a temp file.
renderFile = function(name, download) {
# We make our own tempdir here because it's not safe to delete the result
# of tempdir().
tmpd <- tempfile()
dir.create(tmpd, recursive = TRUE)
self$onSessionEnded(function() unlink(tmpd, recursive = TRUE))
file <- file.path(tmpd, download$filename())
download$content(file)
if (!file.exists(file))
error("downloadHandler for ", name, " did not write a file.")
file
},
# @description Calls `shiny:::flushReact()` and executes all callbacks
# related to reactivity.
flush = function(){
isolate(private$flushCBs$invoke(..stacktraceon = TRUE))
shiny:::flushReact() # namespace to avoid calling our own method
isolate(private$flushedCBs$invoke(..stacktraceon = TRUE))
later::run_now()
},
# @description Produces a warning if the option `shiny.mocksession.warn` is
# unset and not `FALSE`.
# @param name The name of the mocked method.
# @param msg A message describing why the method is not implemented.
noopWarn = function(name, msg) {
if (getOption("shiny.mocksession.warn", FALSE) == FALSE)
return(invisible())
out <- paste0(name, " is not fully implemented by MockShinySession: ", msg)
out <- paste0(out, "\n", "To disable messages like this, run `options(shiny.mocksession.warn=FALSE)`")
warning(out, call. = FALSE)
},
# @description Binds a domain to `expr` and uses `createVarPromiseDomain()`
# to ensure `private$currentOutputName` is set to `name` around any of
# the promise's callbacks. Domains are something like dynamic scopes but
# for promise chains instead of the call stack.
# @return A promise.
withCurrentOutput = function(name, expr) {
if (!is.null(private$currentOutputName)) {
stop("Nested calls to withCurrentOutput() are not allowed.")
}
promises::with_promise_domain(
createVarPromiseDomain(private, "currentOutputName", name),
expr
)
}
),
active = list(
#' @field files For internal use only.
files = function() stop("$files is for internal use only."),
#' @field downloads For internal use only.
downloads = function() stop("$downloads is for internal use only."),
#' @field closed Deprecated in `ShinySession` and signals an error.
closed = function() stop("$closed is deprecated"),
#' @field session Deprecated in ShinySession and signals an error.
session = function() stop("$session is deprecated"),
#' @field request An empty environment where the request should be. The request isn't meaningfully mocked currently.
request = function(value) {
if (!missing(value)){
stop("session$request can't be assigned to")
}
warning("session$request doesn't currently simulate a realistic request on MockShinySession")
new.env(parent=emptyenv())
}
)
)

View File

@@ -29,10 +29,16 @@ 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.
@@ -41,7 +47,7 @@ removeModal <- function(session = getDefaultReactiveDomain()) {
#' @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).
@@ -169,13 +175,8 @@ modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"),
)
}
#' 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

View File

@@ -31,18 +31,148 @@ 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))
}
#' Invoke a Shiny module
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
#' to understand, and modules created with `moduleServer` can be tested with
#' [`testServer()`].
#'
#' @param module A Shiny module server function.
#' @param id An ID string that corresponds with the ID used to call the module's
#' UI function.
#' @param session Session from which to make a child scope (the default should
#' almost always be used).
#'
#' @return The return value, if any, from executing the module server function
#' @seealso <https://shiny.rstudio.com/articles/modules.html>
#'
#' @examples
#' # Define the UI for a module
#' counterUI <- function(id, label = "Counter") {
#' ns <- NS(id)
#' tagList(
#' actionButton(ns("button"), label = label),
#' verbatimTextOutput(ns("out"))
#' )
#' }
#'
#' # Define the server logic for a module
#' counterServer <- function(id) {
#' moduleServer(
#' id,
#' function(input, output, session) {
#' count <- reactiveVal(0)
#' observeEvent(input$button, {
#' count(count() + 1)
#' })
#' output$out <- renderText({
#' count()
#' })
#' count
#' }
#' )
#' }
#'
#' # Use the module in an app
#' ui <- fluidPage(
#' counterUI("counter1", "Counter #1"),
#' counterUI("counter2", "Counter #2")
#' )
#' server <- function(input, output, session) {
#' counterServer("counter1")
#' counterServer("counter2")
#' }
#' if (interactive()) {
#' shinyApp(ui, server)
#' }
#'
#'
#'
#' # If you want to pass extra parameters to the module's server logic, you can
#' # add them to your function. In this case `prefix` is text that will be
#' # printed before the count.
#' counterServer2 <- function(id, prefix = NULL) {
#' moduleServer(
#' id,
#' function(input, output, session) {
#' count <- reactiveVal(0)
#' observeEvent(input$button, {
#' count(count() + 1)
#' })
#' output$out <- renderText({
#' paste0(prefix, count())
#' })
#' count
#' }
#' )
#' }
#'
#' ui <- fluidPage(
#' counterUI("counter", "Counter"),
#' )
#' server <- function(input, output, session) {
#' counterServer2("counter", "The current count is: ")
#' }
#' if (interactive()) {
#' shinyApp(ui, server)
#' }
#'
#' @export
moduleServer <- function(id, module, session = getDefaultReactiveDomain()) {
if (inherits(session, "MockShinySession")) {
body(module) <- rlang::expr({
session$setEnv(base::environment())
!!body(module)
})
session$setReturned(callModule(module, id, session = session))
} else {
callModule(module, id, session = session)
}
}
#' Invoke a Shiny module
#'
#' Note: As of Shiny 1.5.0, we recommend using [`moduleServer()`] instead of
#' [`callModule()`], because the syntax is a little easier
#' to understand, and modules created with `moduleServer` can be tested with
#' [`testServer()`].
#'
#' @param module A Shiny module server function
#' @param id An ID string that corresponds with the ID used to call the module's
@@ -52,9 +182,11 @@ 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>
#' @export
callModule <- function(module, id, ..., session = getDefaultReactiveDomain()) {
if (!inherits(session, c("ShinySession", "session_proxy", "MockShinySession"))) {
stop("session must be a ShinySession or session_proxy object.")
}
childScope <- session$makeScope(id)
withReactiveDomain(childScope, {

View File

@@ -20,50 +20,12 @@
#' [`shinyOptions(progress.style="old")`][shinyOptions] just once, inside the server
#' function.
#'
#' **Methods**
#' \describe{
#' \item{`initialize(session, min = 0, max = 1)`}{
#' Creates a new progress panel (but does not display it).
#' }
#' \item{`set(value = NULL, message = NULL, detail = NULL)`}{
#' Updates the progress panel. When called the first time, the
#' progress panel is displayed.
#' }
#' \item{`inc(amount = 0.1, message = NULL, detail = NULL)`}{
#' Like `set`, this updates the progress panel. The difference is
#' that `inc` increases the progress bar by `amount`, instead
#' of setting it to a specific value.
#' }
#' \item{`close()`}{
#' Removes the progress panel. Future calls to `set` and
#' `close` will be ignored.
#' }
#' }
#'
#' @param session The Shiny session object, as provided by
#' `shinyServer` to the server function.
#' @param min The value that represents the starting point of the
#' progress bar. Must be less than `max`.
#' @param max The value that represents the end of the progress bar.
#' Must be greater than `min`.
#' @param message A single-element character vector; the message to be
#' displayed to the user, or `NULL` to hide the current message
#' (if any).
#' @param detail A single-element character vector; the detail message
#' to be displayed to the user, or `NULL` to hide the current
#' detail message (if any). The detail message will be shown with a
#' de-emphasized appearance relative to `message`.
#' @param value A numeric value at which to set
#' the progress bar, relative to `min` and `max`.
#' @param style Progress display style. If `"notification"` (the default),
#' the progress indicator will show using Shiny's notification API. If
#' `"old"`, use the same HTML and CSS used in Shiny 0.13.2 and below
#' (this is for backward-compatibility).
#' @param amount Single-element numeric vector; the value at which to set
#' the progress bar, relative to `min` and `max`.
#' `NULL` hides the progress bar, if it is currently visible.
#' @param amount For the `inc()` method, a numeric value to increment the
#' progress bar.
#' displayed to the user, or `NULL` to hide the current message (if any).
#' @param detail A single-element character vector; the detail message to be
#' displayed to the user, or `NULL` to hide the current detail message (if
#' any). The detail message will be shown with a de-emphasized appearance
#' relative to `message`.
#'
#' @examples
#' ## Only run examples in interactive R sessions
@@ -99,12 +61,25 @@ Progress <- R6Class(
'Progress',
public = list(
#' @description Creates a new progress panel (but does not display it).
#' @param session The Shiny session object, as provided by `shinyServer` to
#' the server function.
#' @param min The value that represents the starting point of the progress
#' bar. Must be less than `max`.
#' @param max The value that represents the end of the progress bar. Must be
#' greater than `min`.
#' @param style Progress display style. If `"notification"` (the default),
#' the progress indicator will show using Shiny's notification API. If
#' `"old"`, use the same HTML and CSS used in Shiny 0.13.2 and below (this
#' is for backward-compatibility).
initialize = function(session = getDefaultReactiveDomain(),
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)
@@ -117,6 +92,11 @@ Progress <- R6Class(
session$sendProgress('open', list(id = private$id, style = private$style))
},
#' @description Updates the progress panel. When called the first time, the
#' progress panel is displayed.
#' @param value Single-element numeric vector; the value at which to set the
#' progress bar, relative to `min` and `max`. `NULL` hides the progress
#' bar, if it is currently visible.
set = function(value = NULL, message = NULL, detail = NULL) {
if (private$closed) {
warning("Attempting to set progress, but progress already closed.")
@@ -143,6 +123,11 @@ Progress <- R6Class(
private$session$sendProgress('update', data)
},
#' @description Like `set`, this updates the progress panel. The difference
#' is that `inc` increases the progress bar by `amount`, instead of
#' setting it to a specific value.
#' @param amount For the `inc()` method, a numeric value to increment the
#' progress bar.
inc = function(amount = 0.1, message = NULL, detail = NULL) {
if (is.null(private$value))
private$value <- private$min
@@ -151,12 +136,17 @@ Progress <- R6Class(
self$set(value, message, detail)
},
#' @description Returns the minimum value.
getMin = function() private$min,
#' @description Returns the maximum value.
getMax = function() private$max,
#' @description Returns the current value.
getValue = function() private$value,
#' @description Removes the progress panel. Future calls to `set` and
#' `close` will be ignored.
close = function() {
if (private$closed) {
warning("Attempting to close progress, but progress already closed.")
@@ -216,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
@@ -239,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()) {

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()
))
@@ -65,7 +65,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 +87,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 +140,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 +206,8 @@ getCurrentContext <- function() {
.getReactiveEnvironment()$currentContext()
}
hasCurrentContext <- function() {
!is.null(.getReactiveEnvironment()$.currentContext)
!is.null(.getReactiveEnvironment()$.currentContext) ||
isTRUE(getOption("shiny.suppressMissingContextError"))
}
getDummyContext <- function() {

View File

@@ -105,9 +105,7 @@ ReactiveVal <- R6Class(
invisible(TRUE)
},
freeze = function(session = getDefaultReactiveDomain()) {
if (is.null(session)) {
stop("Can't freeze a reactiveVal without a reactive domain")
}
checkReactiveDomain(session)
rLog$freezeReactiveVal(private$reactId, session)
session$onFlushed(function() {
self$thaw(session)
@@ -222,7 +220,7 @@ reactiveVal <- function(value = NULL, label = NULL) {
rv$set(x)
}
},
class = c("reactiveVal", "reactive"),
class = c("reactiveVal", "reactive", "function"),
label = label,
.impl = rv
)
@@ -231,18 +229,30 @@ reactiveVal <- function(value = NULL, label = NULL) {
#' @rdname freezeReactiveValue
#' @export
freezeReactiveVal <- function(x) {
domain <- getDefaultReactiveDomain()
if (is.null(domain)) {
stop("freezeReactiveVal() must be called when a default reactive domain is active.")
if (getOption("shiny.deprecation.messages", TRUE) && getOption("shiny.deprecation.messages.freeze", TRUE)) {
rlang::warn(
"freezeReactiveVal() is soft-deprecated, and may be removed in a future version of Shiny. (See https://github.com/rstudio/shiny/issues/3063)",
.frequency = "once", .frequency_id = "freezeReactiveVal")
}
domain <- getDefaultReactiveDomain()
checkReactiveDomain(domain)
if (!inherits(x, "reactiveVal")) {
stop("x must be a reactiveVal object")
rlang::abort("`x` must be a reactiveVal.")
}
attr(x, ".impl", exact = TRUE)$freeze(domain)
invisible()
}
checkReactiveDomain <- function(x) {
if (is.null(x)) {
rlang::abort("Can't freeze reactive values without a reactive domain.")
}
}
#' @export
format.reactiveVal <- function(x, ...) {
attr(x, ".impl", exact = TRUE)$format(...)
@@ -357,7 +367,7 @@ ReactiveValues <- R6Class(
keyValue
},
set = function(key, value) {
set = function(key, value, force = FALSE) {
# if key exists
# if it is the same value, return
#
@@ -389,10 +399,8 @@ ReactiveValues <- R6Class(
key_exists <- .values$containsKey(key)
if (key_exists) {
if (.dedupe && identical(.values$get(key), value)) {
return(invisible())
}
if (key_exists && !isTRUE(force) && .dedupe && identical(.values$get(key), value)) {
return(invisible())
}
# set the value for better logging
@@ -469,10 +477,15 @@ ReactiveValues <- R6Class(
# Mark a value as frozen If accessed while frozen, a shiny.silent.error will
# be thrown.
freeze = function(key) {
freeze = function(key, invalidate = FALSE) {
domain <- getDefaultReactiveDomain()
rLog$freezeReactiveKey(.reactId, key, domain)
setMeta(key, "frozen", TRUE)
if (invalidate) {
# Force an invalidation
self$set(key, NULL, force = TRUE)
}
},
thaw = function(key) {
@@ -557,7 +570,7 @@ ReactiveValues <- R6Class(
reactiveValues <- function(...) {
args <- list(...)
if ((length(args) > 0) && (is.null(names(args)) || any(names(args) == "")))
stop("All arguments passed to reactiveValues() must be named.")
rlang::abort("All arguments passed to reactiveValues() must be named.")
values <- .createReactiveValues(ReactiveValues$new())
@@ -568,7 +581,7 @@ reactiveValues <- function(...) {
checkName <- function(x) {
if (!is.character(x) || length(x) != 1) {
stop("Must use single string to index into reactivevalues")
rlang::abort("Must use single string to index into reactivevalues.")
}
}
@@ -610,6 +623,14 @@ is.reactivevalues <- function(x) inherits(x, 'reactivevalues')
#' @export
`$.reactivevalues` <- function(x, name) {
checkName(name)
if (!hasCurrentContext()) {
rlang::abort(c(
paste0("Can't access reactive value '", name, "' outside of reactive consumer."),
i = "Do you need to wrap inside reactive() or observe()?"
))
}
.subset2(x, 'impl')$get(.subset2(x, 'ns')(name))
}
@@ -619,7 +640,7 @@ is.reactivevalues <- function(x) inherits(x, 'reactivevalues')
#' @export
`$<-.reactivevalues` <- function(x, name, value) {
if (.subset2(x, 'readonly')) {
stop("Attempted to assign value to a read-only reactivevalues object")
rlang::abort(paste0("Can't modify read-only reactive value '", name, "'"))
}
checkName(name)
.subset2(x, 'impl')$set(.subset2(x, 'ns')(name), value)
@@ -631,12 +652,12 @@ is.reactivevalues <- function(x) inherits(x, 'reactivevalues')
#' @export
`[.reactivevalues` <- function(values, name) {
stop("Single-bracket indexing of reactivevalues object is not allowed.")
rlang::abort("Can't index reactivevalues with `[`.")
}
#' @export
`[<-.reactivevalues` <- function(values, name, value) {
stop("Single-bracket indexing of reactivevalues object is not allowed.")
rlang::abort("Can't index reactivevalues with `[`.")
}
#' @export
@@ -652,16 +673,15 @@ names.reactivevalues <- function(x) {
#' @export
`names<-.reactivevalues` <- function(x, value) {
stop("Can't assign names to reactivevalues object")
rlang::abort("Can't assign names to reactivevalues.")
}
#' @export
as.list.reactivevalues <- function(x, all.names=FALSE, ...) {
shinyDeprecated("reactiveValuesToList",
msg = paste("'as.list.reactivevalues' is deprecated. ",
"Use reactiveValuesToList instead.",
"\nPlease see ?reactiveValuesToList for more information.",
sep = ""))
shinyDeprecated(
"0.4.0", "as.list.reactivevalues()", "reactiveValuesToList()",
details = "Please see ?reactiveValuesToList for more information."
)
reactiveValuesToList(x, all.names)
}
@@ -727,7 +747,10 @@ str.reactivevalues <- function(object, indent.str = " ", ...) {
#' thing that happens if `req(FALSE)` is called. The value is thawed
#' (un-frozen; accessing it will no longer raise an exception) when the current
#' reactive domain is flushed. In a Shiny application, this occurs after all of
#' the observers are executed.
#' the observers are executed. **NOTE:** We are considering deprecating
#' `freezeReactiveVal`, and `freezeReactiveValue` except when `x` is `input`.
#' If this affects your app, please let us know by leaving a comment on
#' [this GitHub issue](https://github.com/rstudio/shiny/issues/3063).
#'
#' @param x For `freezeReactiveValue`, a [reactiveValues()]
#' object (like `input`); for `freezeReactiveVal`, a
@@ -773,9 +796,7 @@ str.reactivevalues <- function(object, indent.str = " ", ...) {
#' @export
freezeReactiveValue <- function(x, name) {
domain <- getDefaultReactiveDomain()
if (is.null(domain)) {
stop("freezeReactiveValue() must be called when a default reactive domain is active.")
}
checkReactiveDomain(domain)
domain$freezeValue(x, name)
invisible()
@@ -807,9 +828,10 @@ Observable <- R6Class(
domain = getDefaultReactiveDomain(),
..stacktraceon = TRUE) {
if (length(formals(func)) > 0)
stop("Can't make a reactive expression from a function that takes one ",
"or more parameters; only functions without parameters can be ",
"reactive.")
rlang::abort(c(
"Can't make a reactive expression from a function that takes arguments.",
"Only functions without parameters can become reactive expressions."
))
# This is to make sure that the function labels that show in the profiler
# and in stack traces doesn't contain whitespace. See
@@ -935,6 +957,7 @@ Observable <- R6Class(
#' @param domain See [domains].
#' @param ..stacktraceon Advanced use only. For stack manipulation purposes; see
#' [stacktrace()].
#' @param ... Not used.
#' @return a function, wrapped in a S3 class "reactive"
#'
#' @examples
@@ -956,20 +979,30 @@ Observable <- R6Class(
#' isolate(reactiveC())
#' isolate(reactiveD())
#' @export
reactive <- function(x, env = parent.frame(), quoted = FALSE, label = NULL,
domain = getDefaultReactiveDomain(),
..stacktraceon = TRUE) {
fun <- exprToFunction(x, env, quoted)
reactive <- function(x, env = parent.frame(), quoted = FALSE,
...,
label = NULL,
domain = getDefaultReactiveDomain(),
..stacktraceon = TRUE)
{
check_dots_empty()
x <- get_quosure(x, env, quoted)
fun <- as_function(x)
# as_function returns a function that takes `...`. We need one that takes no
# args.
formals(fun) <- list()
# Attach a label and a reference to the original user source for debugging
srcref <- attr(substitute(x), "srcref", exact = TRUE)
if (is.null(label)) {
label <- rexprSrcrefToLabel(srcref[[1]],
sprintf('reactive(%s)', paste(deparse(body(fun)), collapse='\n')))
}
if (length(srcref) >= 2) attr(label, "srcref") <- srcref[[2]]
attr(label, "srcfile") <- srcFileOfRef(srcref[[1]])
label <- exprToLabel(get_expr(x), "reactive", label)
o <- Observable$new(fun, label, domain, ..stacktraceon = ..stacktraceon)
structure(o$getValue, observable = o, class = c("reactiveExpr", "reactive"))
structure(
o$getValue,
observable = o,
cacheHint = list(userExpr = zap_srcref(get_expr(x))),
class = c("reactiveExpr", "reactive", "function")
)
}
# Given the srcref to a reactive expression, attempts to figure out what the
@@ -1038,7 +1071,15 @@ execCount <- function(x) {
else if (inherits(x, 'Observer'))
return(x$.execCount)
else
stop('Unexpected argument to execCount')
rlang::abort("Unexpected argument to execCount().")
}
# Internal utility functions for extracting things out of reactives.
reactive_get_value_func <- function(x) {
attr(x, "observable", exact = TRUE)$.origFunc
}
reactive_get_domain <- function(x) {
attr(x, "observable", exact = TRUE)$.domain
}
# Observer ------------------------------------------------------------------
@@ -1070,8 +1111,10 @@ Observer <- R6Class(
domain = getDefaultReactiveDomain(),
autoDestroy = TRUE, ..stacktraceon = TRUE) {
if (length(formals(observerFunc)) > 0)
stop("Can't make an observer from a function that takes parameters; ",
"only functions without parameters can be reactive.")
rlang::abort(c(
"Can't make an observer from a function that takes arguments.",
"Only functions without arguments can become observers."
))
if (grepl("\\s", label, perl = TRUE)) {
funcLabel <- "<observer>"
} else {
@@ -1301,6 +1344,8 @@ Observer <- R6Class(
#' automatically destroyed when its domain (if any) ends.
#' @param ..stacktraceon Advanced use only. For stack manipulation purposes; see
#' [stacktrace()].
#' @param ... Not used.
#'
#' @return An observer reference class object. This object has the following
#' methods:
#' \describe{
@@ -1355,18 +1400,36 @@ Observer <- R6Class(
#' # are at the console, you can force a flush with flushReact()
#' shiny:::flushReact()
#' @export
observe <- function(x, env=parent.frame(), quoted=FALSE, label=NULL,
suspended=FALSE, priority=0,
domain=getDefaultReactiveDomain(), autoDestroy = TRUE,
..stacktraceon = TRUE) {
observe <- function(x, env = parent.frame(), quoted = FALSE,
...,
label = NULL,
suspended = FALSE,
priority = 0,
domain = getDefaultReactiveDomain(),
autoDestroy = TRUE,
..stacktraceon = TRUE)
{
check_dots_empty()
fun <- exprToFunction(x, env, quoted)
if (is.null(label))
label <- sprintf('observe(%s)', paste(deparse(body(fun)), collapse='\n'))
x <- get_quosure(x, env, quoted)
fun <- as_function(x)
# as_function returns a function that takes `...`. We need one that takes no
# args.
formals(fun) <- list()
o <- Observer$new(fun, label=label, suspended=suspended, priority=priority,
domain=domain, autoDestroy=autoDestroy,
..stacktraceon=..stacktraceon)
if (is.null(label)) {
label <- sprintf('observe(%s)', paste(deparse(get_expr(x)), collapse='\n'))
}
o <- Observer$new(
fun,
label = label,
suspended = suspended,
priority = priority,
domain = domain,
autoDestroy = autoDestroy,
..stacktraceon = ..stacktraceon
)
invisible(o)
}
@@ -1377,35 +1440,34 @@ observe <- function(x, env=parent.frame(), quoted=FALSE, label=NULL,
#' already exist; if so, its value will be used as the initial value of the
#' reactive variable (or `NULL` if the variable did not exist).
#'
#' @param symbol A character string indicating the name of the variable that
#' should be made reactive
#' @param env The environment that will contain the reactive variable
#'
#' @param symbol Name of variable to make reactive, as a string.
#' @param env Environment in which to create binding. Expert use only.
#' @return None.
#'
#' @keywords internal
#' @examples
#' \dontrun{
#' reactiveConsole(TRUE)
#'
#' a <- 10
#' makeReactiveBinding("a")
#'
#' b <- reactive(a * -1)
#' observe(print(b()))
#'
#' a <- 20
#' }
#' a <- 30
#'
#' reactiveConsole(FALSE)
#' @export
makeReactiveBinding <- function(symbol, env = parent.frame()) {
if (exists(symbol, envir = env, inherits = FALSE)) {
initialValue <- env[[symbol]]
rm(list = symbol, envir = env, inherits = FALSE)
}
else
} else {
initialValue <- NULL
values <- reactiveValues(value = initialValue)
makeActiveBinding(symbol, env=env, fun=function(v) {
if (missing(v))
values$value
else
values$value <- v
})
}
val <- reactiveVal(initialValue, label = symbol)
makeActiveBinding(symbol, val, env = env)
invisible()
}
@@ -1441,6 +1503,29 @@ setAutoflush <- local({
}
})
#' Activate reactivity in the console
#'
#' This is an experimental feature that allows you to enable reactivity
#' at the console, for the purposes of experimentation and learning.
#'
#' @keywords internal
#' @param enabled Turn console reactivity on or off?
#' @export
#' @examples
#' reactiveConsole(TRUE)
#' x <- reactiveVal(10)
#' y <- observe({
#' message("The value of x is ", x())
#' })
#' x(20)
#' x(30)
#' reactiveConsole(FALSE)
reactiveConsole <- function(enabled) {
options(shiny.suppressMissingContextError = enabled)
setAutoflush(enabled)
}
# ---------------------------------------------------------------------------
#' Timer
@@ -1513,14 +1598,16 @@ reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain())
# reactId <- nextGlobalReactId()
# rLog$define(reactId, paste0("timer(", intervalMs, ")"))
scheduler <- defineScheduler(session)
dependents <- Map$new()
timerHandle <- scheduleTask(intervalMs, function() {
timerHandle <- scheduler(intervalMs, function() {
# Quit if the session is closed
if (!is.null(session) && session$isClosed()) {
return(invisible())
}
timerHandle <<- scheduleTask(intervalMs, sys.function())
timerHandle <<- scheduler(intervalMs, sys.function())
doInvalidate <- function() {
lapply(
@@ -1613,7 +1700,6 @@ reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain())
#' }
#' @export
invalidateLater <- function(millis, session = getDefaultReactiveDomain()) {
force(session)
ctx <- getCurrentContext()
@@ -1621,7 +1707,9 @@ invalidateLater <- function(millis, session = getDefaultReactiveDomain()) {
clear_on_ended_callback <- function() {}
timerHandle <- scheduleTask(millis, function() {
scheduler <- defineScheduler(session)
timerHandle <- scheduler(millis, function() {
if (is.null(session)) {
ctx$invalidate()
return(invisible())
@@ -1734,6 +1822,7 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
rv <- reactiveValues(cookie = isolate(checkFunc()))
re_finalized <- FALSE
env <- environment()
o <- observe({
# When no one holds a reference to the reactive returned from
@@ -1741,7 +1830,7 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
# firing and hold onto resources.
if (re_finalized) {
o$destroy()
rm(o, envir = parent.env(environment()))
rm(o, envir = env)
return()
}
@@ -1945,7 +2034,11 @@ maskReactiveContext <- function(expr) {
#' Event handler
#'
#' Respond to "event-like" reactive inputs, values, and expressions.
#' Respond to "event-like" reactive inputs, values, and expressions. As of Shiny
#' 1.6.0, we recommend using [bindEvent()] instead of `eventReactive()` and
#' `observeEvent()`. This is because `bindEvent()` can be composed with
#' [bindCache()], and because it can also be used with `render` functions (like
#' [renderText()] and [renderPlot()]).
#'
#' Shiny's reactive programming framework is primarily designed for calculated
#' values (reactive expressions) and side-effect-causing actions (observers)
@@ -1967,13 +2060,17 @@ maskReactiveContext <- function(expr) {
#' response to an event. (Note that "recalculate a value" does not generally
#' count as performing an action--see `eventReactive` for that.) The first
#' argument is the event you want to respond to, and the second argument is a
#' function that should be called whenever the event occurs.
#' function that should be called whenever the event occurs. Note that
#' `observeEvent()` is equivalent to using `observe() %>% bindEvent()` and as of
#' Shiny 1.6.0, we recommend the latter.
#'
#' Use `eventReactive` 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.
#' in response to the given event. Note that
#' `eventReactive()` is equivalent to using `reactive() %>% bindEvent()` and as of
#' Shiny 1.6.0, we recommend the latter.
#'
#' @section ignoreNULL and ignoreInit:
#'
@@ -2007,6 +2104,7 @@ maskReactiveContext <- function(expr) {
#' 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`}{
@@ -2084,6 +2182,7 @@ maskReactiveContext <- function(expr) {
#' after the first time that the code in `handlerExpr` is run. This
#' pattern is useful when you want to subscribe to a event that should only
#' happen once.
#' @param ... Currently not used.
#'
#' @return `observeEvent` returns an observer reference class object (see
#' [observe()]). `eventReactive` returns a reactive expression
@@ -2092,7 +2191,7 @@ maskReactiveContext <- function(expr) {
#' @seealso [actionButton()]
#'
#' @examples
#' ## Only run this example in interactive R sessions
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
#'
#' ## App 1: Sample usage
@@ -2111,6 +2210,12 @@ maskReactiveContext <- function(expr) {
#' observeEvent(input$button, {
#' cat("Showing", input$x, "rows\n")
#' })
#' # The observeEvent() above is equivalent to:
#' # observe({
#' # cat("Showing", input$x, "rows\n")
#' # }) %>%
#' # bindEvent(input$button)
#'
#' # Take a reactive dependency on input$button, but
#' # not on any of the stuff inside the function
#' df <- eventReactive(input$button, {
@@ -2130,6 +2235,12 @@ maskReactiveContext <- function(expr) {
#' print(paste("This will only be printed once; all",
#' "subsequent button clicks won't do anything"))
#' }, once = TRUE)
#' # The observeEvent() above is equivalent to:
#' # observe({
#' # print(paste("This will only be printed once; all",
#' # "subsequent button clicks won't do anything"))
#' # }) %>%
#' # bindEvent(input$go, once = TRUE)
#' }
#' )
#'
@@ -2156,42 +2267,38 @@ maskReactiveContext <- function(expr) {
observeEvent <- function(eventExpr, handlerExpr,
event.env = parent.frame(), event.quoted = FALSE,
handler.env = parent.frame(), handler.quoted = FALSE,
...,
label = NULL, suspended = FALSE, priority = 0,
domain = getDefaultReactiveDomain(), autoDestroy = TRUE,
ignoreNULL = TRUE, ignoreInit = FALSE, once = FALSE) {
ignoreNULL = TRUE, ignoreInit = FALSE, once = FALSE)
{
check_dots_empty()
eventFunc <- exprToFunction(eventExpr, event.env, event.quoted)
if (is.null(label))
label <- sprintf('observeEvent(%s)', paste(deparse(body(eventFunc)), collapse='\n'))
eventFunc <- wrapFunctionLabel(eventFunc, "observeEventExpr", ..stacktraceon = TRUE)
eventExpr <- get_quosure(eventExpr, event.env, event.quoted)
handlerExpr <- get_quosure(handlerExpr, handler.env, handler.quoted)
handlerFunc <- exprToFunction(handlerExpr, handler.env, handler.quoted)
handlerFunc <- wrapFunctionLabel(handlerFunc, "observeEventHandler", ..stacktraceon = TRUE)
if (is.null(label)) {
label <- sprintf('observeEvent(%s)', paste(deparse(get_expr(eventExpr)), collapse='\n'))
}
initialized <- FALSE
handler <- inject(observe(
!!handlerExpr,
label = label,
suspended = suspended,
priority = priority,
domain = domain,
autoDestroy = TRUE,
..stacktraceon = FALSE # TODO: Does this go in the bindEvent?
))
o <- observe({
hybrid_chain(
{eventFunc()},
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
return()
}
if (ignoreNULL && isNullEvent(value)) {
return()
}
if (once) {
on.exit(o$destroy())
}
isolate(handlerFunc())
}
)
}, label = label, suspended = suspended, priority = priority, domain = domain,
autoDestroy = TRUE, ..stacktraceon = FALSE)
o <- inject(bindEvent(
ignoreNULL = ignoreNULL,
ignoreInit = ignoreInit,
once = once,
label = label,
!!eventExpr,
x = handler
))
invisible(o)
}
@@ -2201,34 +2308,26 @@ observeEvent <- function(eventExpr, handlerExpr,
eventReactive <- function(eventExpr, valueExpr,
event.env = parent.frame(), event.quoted = FALSE,
value.env = parent.frame(), value.quoted = FALSE,
...,
label = NULL, domain = getDefaultReactiveDomain(),
ignoreNULL = TRUE, ignoreInit = FALSE) {
ignoreNULL = TRUE, ignoreInit = FALSE)
{
check_dots_empty()
eventFunc <- exprToFunction(eventExpr, event.env, event.quoted)
if (is.null(label))
label <- sprintf('eventReactive(%s)', paste(deparse(body(eventFunc)), collapse='\n'))
eventFunc <- wrapFunctionLabel(eventFunc, "eventReactiveExpr", ..stacktraceon = TRUE)
eventExpr <- get_quosure(eventExpr, event.env, event.quoted)
valueExpr <- get_quosure(valueExpr, value.env, value.quoted)
handlerFunc <- exprToFunction(valueExpr, value.env, value.quoted)
handlerFunc <- wrapFunctionLabel(handlerFunc, "eventReactiveHandler", ..stacktraceon = TRUE)
if (is.null(label)) {
label <- sprintf('eventReactive(%s)', paste(deparse(get_expr(eventExpr)), collapse='\n'))
}
initialized <- FALSE
invisible(reactive({
hybrid_chain(
eventFunc(),
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
req(FALSE)
}
req(!ignoreNULL || !isNullEvent(value))
isolate(handlerFunc())
}
)
}, label = label, domain = domain, ..stacktraceon = FALSE))
invisible(inject(bindEvent(
ignoreNULL = ignoreNULL,
ignoreInit = ignoreInit,
label = label,
!!eventExpr,
x = reactive(!!valueExpr, domain = domain, label = label)
)))
}
isNullEvent <- function(value) {
@@ -2359,20 +2458,24 @@ debounce <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
when = NULL # the deadline for the timer to fire; NULL if not scheduled
)
# Responsible for tracking when f() changes.
# Responsible for tracking when r() changes.
firstRun <- TRUE
observe({
r()
if (firstRun) {
# During the first run we don't want to set v$when, as this will kick off
# the timer. We only want to do that when we see r() change.
firstRun <<- FALSE
# Ensure r() is called only after setting firstRun to FALSE since r()
# may throw an error
r()
return()
}
# This ensures r() is still tracked after firstRun
r()
# The value (or possibly millis) changed. Start or reset the timer.
v$when <- Sys.time() + millis()/1000
v$when <- getDomainTimeMs(domain) + millis()
}, label = "debounce tracker", domain = domain, priority = priority)
# This observer is the timer. It rests until v$when elapses, then touches
@@ -2381,13 +2484,13 @@ debounce <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
if (is.null(v$when))
return()
now <- Sys.time()
now <- getDomainTimeMs(domain)
if (now >= v$when) {
# Mod by 999999999 to get predictable overflow behavior
v$trigger <- isolate(v$trigger %OR% 0) %% 999999999 + 1
v$trigger <- isolate(v$trigger %||% 0) %% 999999999 + 1
v$when <- NULL
} else {
invalidateLater((v$when - now) * 1000)
invalidateLater(v$when - now)
}
}, label = "debounce timer", domain = domain, priority = priority)
@@ -2432,12 +2535,12 @@ throttle <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
if (is.null(v$lastTriggeredAt)) {
0
} else {
max(0, (v$lastTriggeredAt + millis()/1000) - Sys.time()) * 1000
max(0, v$lastTriggeredAt + millis() - getDomainTimeMs(domain))
}
}
trigger <- function() {
v$lastTriggeredAt <- Sys.time()
v$lastTriggeredAt <- getDomainTimeMs(domain)
# Mod by 999999999 to get predictable overflow behavior
v$trigger <- isolate(v$trigger) %% 999999999 + 1
v$pending <- FALSE

195
R/reexports.R Normal file
View File

@@ -0,0 +1,195 @@
####
# Generated by `./tools/updateReexports.R`: do not edit by hand
# Please call `source('tools/updateReexports.R') from the root folder to update`
####
# fastmap key_missing.Rd -------------------------------------------------------
#' @importFrom fastmap key_missing
#' @export
fastmap::key_missing
#' @importFrom fastmap is.key_missing
#' @export
fastmap::is.key_missing
# htmltools builder.Rd ---------------------------------------------------------
#' @importFrom htmltools tags
#' @export
htmltools::tags
#' @importFrom htmltools p
#' @export
htmltools::p
#' @importFrom htmltools h1
#' @export
htmltools::h1
#' @importFrom htmltools h2
#' @export
htmltools::h2
#' @importFrom htmltools h3
#' @export
htmltools::h3
#' @importFrom htmltools h4
#' @export
htmltools::h4
#' @importFrom htmltools h5
#' @export
htmltools::h5
#' @importFrom htmltools h6
#' @export
htmltools::h6
#' @importFrom htmltools a
#' @export
htmltools::a
#' @importFrom htmltools br
#' @export
htmltools::br
#' @importFrom htmltools div
#' @export
htmltools::div
#' @importFrom htmltools span
#' @export
htmltools::span
#' @importFrom htmltools pre
#' @export
htmltools::pre
#' @importFrom htmltools code
#' @export
htmltools::code
#' @importFrom htmltools img
#' @export
htmltools::img
#' @importFrom htmltools strong
#' @export
htmltools::strong
#' @importFrom htmltools em
#' @export
htmltools::em
#' @importFrom htmltools hr
#' @export
htmltools::hr
# htmltools tag.Rd -------------------------------------------------------------
#' @importFrom htmltools tag
#' @export
htmltools::tag
#' @importFrom htmltools tagList
#' @export
htmltools::tagList
#' @importFrom htmltools tagAppendAttributes
#' @export
htmltools::tagAppendAttributes
#' @importFrom htmltools tagHasAttribute
#' @export
htmltools::tagHasAttribute
#' @importFrom htmltools tagGetAttribute
#' @export
htmltools::tagGetAttribute
#' @importFrom htmltools tagAppendChild
#' @export
htmltools::tagAppendChild
#' @importFrom htmltools tagAppendChildren
#' @export
htmltools::tagAppendChildren
#' @importFrom htmltools tagSetChildren
#' @export
htmltools::tagSetChildren
# htmltools HTML.Rd ------------------------------------------------------------
#' @importFrom htmltools HTML
#' @export
htmltools::HTML
# htmltools include.Rd ---------------------------------------------------------
#' @importFrom htmltools includeHTML
#' @export
htmltools::includeHTML
#' @importFrom htmltools includeText
#' @export
htmltools::includeText
#' @importFrom htmltools includeMarkdown
#' @export
htmltools::includeMarkdown
#' @importFrom htmltools includeCSS
#' @export
htmltools::includeCSS
#' @importFrom htmltools includeScript
#' @export
htmltools::includeScript
# htmltools singleton.Rd -------------------------------------------------------
#' @importFrom htmltools singleton
#' @export
htmltools::singleton
#' @importFrom htmltools is.singleton
#' @export
htmltools::is.singleton
# htmltools validateCssUnit.Rd -------------------------------------------------
#' @importFrom htmltools validateCssUnit
#' @export
htmltools::validateCssUnit
# htmltools htmlTemplate.Rd ----------------------------------------------------
#' @importFrom htmltools htmlTemplate
#' @export
htmltools::htmlTemplate
# htmltools suppressDependencies.Rd --------------------------------------------
#' @importFrom htmltools suppressDependencies
#' @export
htmltools::suppressDependencies
# htmltools withTags.Rd --------------------------------------------------------
#' @importFrom htmltools withTags
#' @export
htmltools::withTags

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

@@ -36,6 +36,12 @@
#' @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
#' rendering in R; it won't change the actual ppi of the browser.
#' @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.
#' NULL or "" is not recommended because those should be limited to decorative images
#' (the default is "Plot object").
#' @param ... Arguments to be passed through to [grDevices::png()].
#' These can be used to set the width, height, background color, etc.
#' @param env The environment in which to evaluate `expr`.
@@ -51,13 +57,16 @@
#' 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 = "Plot object",
env = parent.frame(), quoted = FALSE,
execOnResize = FALSE, outputArgs = list()
) {
expr <- get_quosure(expr, env, quoted)
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
# is called
installExprFunction(expr, "func", env, quoted, ..stacktraceon = TRUE)
func <- quoToFunction(expr, "renderPlot", ..stacktraceon = TRUE)
args <- list(...)
@@ -75,7 +84,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 +112,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 +123,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 +131,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 +150,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,15 +180,27 @@ 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 = get_expr(expr), res = res)
)
class(markedFunc) <- c("shiny.renderPlot", class(markedFunc))
markedFunc
}
resizeSavedPlot <- function(name, session, result, width, height, pixelratio, res, ...) {
resizeSavedPlot <- function(name, session, result, width, height, alt, pixelratio, res, ...) {
if (result$img$width == width && result$img$height == height &&
result$pixelratio == pixelratio && result$res == res) {
return(result)
}
if (isNamespaceLoaded("showtext")) {
showtextOpts <- showtext::showtext_opts(dpi = res*pixelratio)
on.exit({showtext::showtext_opts(showtextOpts)}, add = TRUE)
}
coordmap <- NULL
outfile <- plotPNG(function() {
grDevices::replayPlot(result$recordedPlot)
@@ -176,6 +212,7 @@ resizeSavedPlot <- function(name, session, result, width, height, pixelratio, re
src = session$fileUrl(name, outfile, contentType = "image/png"),
width = width,
height = height,
alt = alt,
coordmap = coordmap,
error = attr(coordmap, "error", exact = TRUE)
)
@@ -183,7 +220,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
@@ -200,13 +237,25 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
domain <- createGraphicsDevicePromiseDomain(device)
grDevices::dev.control(displaylist = "enable")
# In some cases (at least when `png(type='cairo')), showtext's font
# rendering needs to know about the device's resolution to work properly.
# I don't see any immediate harm in setting the dpi option for any device,
# but it's worth noting that the option doesn't currently work with CairoPNG.
# https://github.com/yixuan/showtext/issues/33
showtextOpts <- if (isNamespaceLoaded("showtext")) {
showtext::showtext_opts(dpi = res*pixelratio)
} else {
NULL
}
hybrid_chain(
hybrid_chain(
promises::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
@@ -224,7 +273,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
@@ -246,6 +295,9 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
}),
finally = function() {
grDevices::dev.off(device)
if (length(showtextOpts)) {
showtext::showtext_opts(showtextOpts)
}
}
),
function(result) {
@@ -253,6 +305,7 @@ drawPlot <- function(name, session, func, width, height, pixelratio, res, ...) {
src = session$fileUrl(name, outfile, contentType='image/png'),
width = width,
height = height,
alt = alt,
coordmap = result$coordmap,
# Get coordmap error message if present
error = attr(result$coordmap, "error", exact = TRUE)
@@ -557,6 +610,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.
@@ -573,8 +630,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
)
}
@@ -889,6 +946,14 @@ find_panel_info_non_api <- function(b, ggplot_format) {
})
}
# Use public API for getting the unit's type (grid::unitType(), added in R 4.0)
# https://github.com/wch/r-source/blob/f9b8a42/src/library/grid/R/unit.R#L179
getUnitType <- function(u) {
tryCatch(
get("unitType", envir = asNamespace("grid"))(u),
error = function(e) attr(u, "unit", exact = TRUE)
)
}
# Given a gtable object, return the x and y ranges (in pixel dimensions)
find_panel_ranges <- function(g, res) {
@@ -904,11 +969,11 @@ find_panel_ranges <- function(g, res) {
if (inherits(x, "unit.list")) {
# For ggplot2 <= 1.0.1
vapply(x, FUN.VALUE = logical(1), function(u) {
isTRUE(attr(u, "unit", exact = TRUE) == "null")
isTRUE(getUnitType(u) == "null")
})
} else {
# For later versions of ggplot2
attr(x, "unit", exact = TRUE) == "null"
getUnitType(x) == "null"
}
}
@@ -948,7 +1013,11 @@ find_panel_ranges <- function(g, res) {
# The plotting panels all are 'null' units.
null_sizes <- rep(NA_real_, length(rel_sizes))
null_sizes[null_idx] <- as.numeric(rel_sizes[null_idx])
# Workaround for `[.unit` forbidding zero-length subsets
# https://github.com/wch/r-source/blob/f9b8a42/src/library/grid/R/unit.R#L448-L450
if (length(null_idx)) {
null_sizes[null_idx] <- as.numeric(rel_sizes[null_idx])
}
# Total size allocated for panels is the total image size minus absolute
# (non-panel) elements.

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()].
@@ -47,14 +49,33 @@
#' 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())
{
expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderTable")
if (!is.function(spacing)) spacing <- match.arg(spacing)

562
R/runapp.R Normal file
View File

@@ -0,0 +1,562 @@
#' 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 (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
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
}
}
}
# ============================================================================
# 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
#' `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
}

View File

@@ -5,14 +5,15 @@ inputHandlers <- Map$new()
#'
#' Adds an input handler for data of this type. When called, Shiny will use the
#' function provided to refine the data passed back from the client (after being
#' deserialized by jsonlite) before making it available in the `input`
#' variable of the `server.R` file.
#' deserialized by jsonlite) before making it available in the `input` variable
#' of the `server.R` file.
#'
#' This function will register the handler for the duration of the R process
#' (unless Shiny is explicitly reloaded). For that reason, the `type` used
#' should be very specific to this package to minimize the risk of colliding
#' with another Shiny package which might use this data type name. We recommend
#' the format of "packageName.widgetName".
#' the format of "packageName.widgetName". It should be called from the
#' package's `.onLoad()` function.
#'
#' Currently Shiny registers the following handlers: `shiny.matrix`,
#' `shiny.number`, and `shiny.date`.
@@ -20,23 +21,20 @@ inputHandlers <- Map$new()
#' The `type` of a custom Shiny Input widget will be deduced using the
#' `getType()` JavaScript function on the registered Shiny inputBinding.
#' @param type The type for which the handler should be added --- should be a
#' single-element character vector.
#' single-element character vector.
#' @param fun The handler function. This is the function that will be used to
#' parse the data delivered from the client before it is available in the
#' `input` variable. The function will be called with the following three
#' parameters:
#' \enumerate{
#' \item{The value of this input as provided by the client, deserialized
#' using jsonlite.}
#' \item{The `shinysession` in which the input exists.}
#' \item{The name of the input.}
#' }
#' @param force If `TRUE`, will overwrite any existing handler without
#' warning. If `FALSE`, will throw an error if this class already has
#' a handler defined.
#' parameters: \enumerate{ \item{The value of this input as provided by the
#' client, deserialized using jsonlite.} \item{The `shinysession` in which the
#' input exists.} \item{The name of the input.} }
#' @param force If `TRUE`, will overwrite any existing handler without warning.
#' If `FALSE`, will throw an error if this class already has a handler
#' defined.
#' @examples
#' \dontrun{
#' # Register an input handler which rounds a input number to the nearest integer
#' # In a package, this should be called from the .onLoad function.
#' registerInputHandler("mypackage.validint", function(x, shinysession, name) {
#' if (is.null(x)) return(NA)
#' round(x)

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

@@ -22,184 +22,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('^[a-z0-9\\-_][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 \lifecycle{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.
@@ -217,7 +49,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://rstudio.github.io/shiny/tutorial/) for more
#' on how to write a server function.
#'
#' @param func The server function for this application. See the details section
@@ -246,6 +78,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)
}
@@ -279,6 +122,8 @@ decodeMessage <- function(data) {
return(mainMessage)
}
autoReloadCallbacks <- Callbacks$new()
createAppHandlers <- function(httpHandlers, serverFuncSource) {
appvars <- new.env()
appvars$server <- NULL
@@ -304,6 +149,22 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
return(TRUE)
}
if (identical(ws$request$PATH_INFO, "/autoreload/")) {
if (!get_devmode_option("shiny.autoreload", FALSE)) {
ws$close()
return(TRUE)
}
callbackHandle <- autoReloadCallbacks$register(function() {
ws$send("autoreload")
ws$close()
})
ws$onClose(function() {
callbackHandle()
})
return(TRUE)
}
if (!is.null(getOption("shiny.observer.error", NULL))) {
warning(
call. = FALSE,
@@ -626,9 +487,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.
@@ -637,589 +495,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())
}
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)
}
require(shiny)
# 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
}
}
}
# Invoke user-defined onStop callbacks, before the application's internal
# onStop callbacks.
on.exit({
.globals$onStopCallbacks$invoke()
.globals$onStopCallbacks <- Callbacks$new()
}, add = TRUE)
# 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 port The TCP port that the application should listen on. Defaults to
#' choosing 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.
#' @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.
#'
#' @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=NULL,
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.
@@ -1230,5 +508,5 @@ inShinyServer <- function() {
# This check was moved out of the main function body because of an issue with
# the RStudio debugger. (#1474)
isEmptyMessage <- function(msg) {
identical(charToRaw("\003\xe9"), msg)
identical(as.raw(c(0x03, 0xe9)), msg)
}

View File

@@ -8,145 +8,249 @@ 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
#' app directory will be continually monitored for changes to files that
#' have the extensions: r, htm, html, js, css, png, jpg, jpeg, gif. If any
#' changes are detected, all connected Shiny sessions are reloaded. This
#' allows for fast feedback loops when tweaking Shiny UI.
#' \item{shiny.autoreload (defaults to `FALSE`)}{If `TRUE` when a Shiny app is launched, the
#' app directory will be continually monitored for changes to files that
#' have the extensions: r, htm, html, js, css, png, jpg, jpeg, gif. If any
#' 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.
#' Since monitoring for changes is expensive (we simply poll for last
#' modified times), this feature is 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"))`
#' 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"))`
#'
#' The default polling interval is 500 milliseconds. You can change this
#' by setting e.g. `options(shiny.autoreload.interval = 2000)` (every
#' two seconds).}
#' \item{shiny.deprecation.messages (defaults to `TRUE`)}{This controls whether messages for
#' deprecated functions in Shiny will be printed. See
#' [shinyDeprecated()] for more information.}
#' \item{shiny.error (defaults to `NULL`)}{This can be a function which is called when an error
#' occurs. For example, `options(shiny.error=recover)` will result a
#' the debugger prompt when an error occurs.}
#' \item{shiny.fullstacktrace (defaults to `FALSE`)}{Controls whether "pretty" (`FALSE`) or full
#' stack traces (`TRUE`) are dumped to the console when errors occur during Shiny app execution.
#' Pretty stack traces attempt to only show user-supplied code, but this pruning can't always
#' be done 100\% correctly.}
#' \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.
#' The default polling interval is 500 milliseconds. You can change this
#' by setting e.g. `options(shiny.autoreload.interval = 2000)` (every
#' two seconds).}
#' \item{shiny.deprecation.messages (defaults to `TRUE`)}{This controls whether messages for
#' deprecated functions in Shiny will be printed. See
#' [shinyDeprecated()] for more information.}
#' \item{shiny.error (defaults to `NULL`)}{This can be a function which is called when an error
#' occurs. For example, `options(shiny.error=recover)` will result a
#' the debugger prompt when an error occurs.}
#' \item{shiny.fullstacktrace (defaults to `FALSE`)}{Controls whether "pretty" (`FALSE`) or full
#' stack traces (`TRUE`) are dumped to the console when errors occur during Shiny app execution.
#' Pretty stack traces attempt to only show user-supplied code, but this pruning can't always
#' be done 100% correctly.}
#' \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.4.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.}
#' \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.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
#' Whether or not to include Shiny's JavaScript as a minified (`shiny.min.js`)
#' or un-minified (`shiny.js`) file. The un-minified version is larger,
#' but can be helpful for development and debugging.}
#' \item{shiny.port (defaults to a random open port)}{A port number that Shiny will listen on. See
#' [runApp()] for more information.}
#' \item{shiny.reactlog (defaults to `FALSE`)}{If `TRUE`, enable logging of reactive events,
#' which can be viewed later with the [reactlogShow()] function.
#' This incurs a substantial performance penalty and should not be used in
#' 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
#' 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
#' user to see.}
#' \item{shiny.stacktraceoffset (defaults to `TRUE`)}{If `TRUE`, then Shiny's printed stack
#' traces will display srcrefs one line above their usual location. This is
#' an arguably more intuitive arrangement for casual R users, as the name
#' of a function appears next to the srcref where it is defined, rather than
#' where it is currently being called from.}
#' \item{shiny.suppressMissingContextError (defaults to `FALSE`)}{Normally, invoking a reactive
#' outside of a reactive context (or [isolate()]) results in
#' an error. If this is `TRUE`, don't error in these cases. This
#' should only be used for debugging or demonstrations of reactivity at the
#' console.}
#' \item{shiny.testmode (defaults to `FALSE`)}{If `TRUE`, then various features for testing Shiny
#' applications are enabled.}
#' \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.}
#' then jQuery `r version_jquery` 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.}
#' \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.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
#' Whether or not to include Shiny's JavaScript as a minified (`shiny.min.js`)
#' or un-minified (`shiny.js`) file. The un-minified version is larger,
#' but can be helpful for development and debugging.}
#' \item{shiny.port (defaults to a random open port)}{A port number that Shiny will listen on. See
#' [runApp()] for more information.}
#' \item{shiny.reactlog (defaults to `FALSE`)}{If `TRUE`, enable logging of reactive events,
#' which can be viewed later with the [reactlogShow()] function.
#' This incurs a substantial performance penalty and should not be used in
#' 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
#' 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
#' user to see.}
#' \item{shiny.stacktraceoffset (defaults to `TRUE`)}{If `TRUE`, then Shiny's printed stack
#' traces will display srcrefs one line above their usual location. This is
#' an arguably more intuitive arrangement for casual R users, as the name
#' of a function appears next to the srcref where it is defined, rather than
#' where it is currently being called from.}
#' \item{shiny.suppressMissingContextError (defaults to `FALSE`)}{Normally, invoking a reactive
#' outside of a reactive context (or [isolate()]) results in
#' an error. If this is `TRUE`, don't error in these cases. This
#' should only be used for debugging or demonstrations of reactivity at the
#' console.}
#' \item{shiny.testmode (defaults to `FALSE`)}{If `TRUE`, then various features for testing Shiny
#' applications are enabled.}
#' \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.autoload.r (defaults to `TRUE`)}{If `TRUE`, then the R/
#' of a shiny app will automatically be sourced.}
#' \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.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')
#' }
#'
#'
#' @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(...)
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 +261,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)
}

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

@@ -0,0 +1,34 @@
# See also R/reexports.R
## usethis namespace: start
## usethis namespace: end
#' @importFrom lifecycle deprecated
#' @importFrom grDevices dev.set dev.cur
#' @importFrom fastmap fastmap
#' @importFrom promises %...!%
#' @importFrom promises %...>%
#' @importFrom promises
#' promise promise_resolve promise_reject is.promising
#' as.promise
#' @importFrom rlang
#' quo enquo as_function get_expr get_env new_function enquos
#' eval_tidy expr pairlist2 new_quosure enexpr as_quosure is_quosure inject
#' enquos0 zap_srcref %||% is_na
#' is_false list2
#' missing_arg is_missing maybe_missing
#' @importFrom ellipsis
#' check_dots_empty check_dots_unnamed
#' @import htmltools
#' @import httpuv
#' @import xtable
#' @import R6
#' @import mime
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

524
R/shiny.R
View File

@@ -1,4 +1,4 @@
#' @include utils.R stack.R
#' @include utils.R
NULL
#' Web Application Framework for R
@@ -8,7 +8,7 @@ NULL
#' prebuilt widgets make it possible to build beautiful, responsive, and
#' powerful applications with minimal effort.
#'
#' The Shiny tutorial at <http://shiny.rstudio.com/tutorial/> explains
#' The Shiny tutorial at <https://shiny.rstudio.com/tutorial/> explains
#' the framework in depth, walks you through building a simple application, and
#' includes extensive annotated examples.
#'
@@ -17,15 +17,6 @@ NULL
#' @name shiny-package
#' @aliases shiny
#' @docType package
#' @import htmltools httpuv xtable digest R6 mime
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
createUniqueId <- function(bytes, prefix = "", suffix = "") {
@@ -117,9 +108,6 @@ workerId <- local({
#' \item{clientData}{
#' A [reactiveValues()] object that contains information about the client.
#' \itemize{
#' \item{`allowDataUriScheme` is a logical value that indicates whether
#' the browser is able to handle URIs that use the `data:` scheme.
#' }
#' \item{`pixelratio` reports the "device pixel ratio" from the web browser,
#' or 1 if none is reported. The value is 2 for Apple Retina displays.
#' }
@@ -205,6 +193,14 @@ workerId <- local({
#' An environment for app authors and module/package authors to store whatever
#' session-specific data they want.
#' }
#' \item{user}{
#' User's log-in information. Useful for identifying users on hosted platforms
#' such as RStudio Connect and Shiny Server.
#' }
#' \item{groups}{
#' The `user`'s relevant group information. Useful for determining what
#' privileges the user should or shouldn't have.
#' }
#' \item{resetBrush(brushId)}{
#' Resets/clears the brush with the given `brushId`, if it exists on
#' any `imageOutput` or `plotOutput` in the app.
@@ -272,6 +268,18 @@ workerId <- local({
#' character vector, as in `input=c("x", "y")`. The format can be
#' "rds" or "json".
#' }
#' \item{setCurrentTheme(theme)}{
#' Sets the current [bootstrapLib()] theme, which updates the value of
#' [getCurrentTheme()], invalidates `session$getCurrentTheme()`, and calls
#' function(s) registered with [registerThemeDependency()] with provided
#' `theme`. If those function calls return [htmltools::htmlDependency()]s with
#' `stylesheet`s, then those stylesheets are "refreshed" (i.e., the new
#' stylesheets are inserted on the page and the old ones are disabled and
#' removed).
#' }
#' \item{getCurrentTheme()}{
#' A reactive read of the current [bootstrapLib()] theme.
#' }
#'
#' @name session
NULL
@@ -280,7 +288,7 @@ NULL
#'
#' The `NS` function creates namespaced IDs out of bare IDs, by joining
#' them using `ns.sep` as the delimiter. It is intended for use in Shiny
#' modules. See <http://shiny.rstudio.com/articles/modules.html>.
#' modules. See <https://shiny.rstudio.com/articles/modules.html>.
#'
#' Shiny applications use IDs to identify inputs and outputs. These IDs must be
#' unique within an application, as accidentally using the same input/output ID
@@ -297,7 +305,7 @@ NULL
#' @param id The id string to be namespaced (optional).
#' @return If `id` is missing, returns a function that expects an id string
#' as its only argument and returns that id with the namespace prepended.
#' @seealso <http://shiny.rstudio.com/articles/modules.html>
#' @seealso <https://shiny.rstudio.com/articles/modules.html>
#' @export
NS <- function(namespace, id = NULL) {
if (length(namespace) == 0)
@@ -335,8 +343,8 @@ ShinySession <- R6Class(
websocket = 'ANY',
invalidatedOutputValues = 'Map',
invalidatedOutputErrors = 'Map',
inputMessageQueue = list(), # A list of inputMessages to send when flushed
cycleStartActionQueue = list(), # A list of actions to perform to start a cycle
inputMessageQueue = 'fastqueue', # A list of inputMessages to send when flushed
cycleStartActionQueue = 'fastqueue', # A list of actions to perform to start a cycle
.outputs = list(), # Keeps track of all the output observer objects
.outputOptions = list(), # Options for each of the output observer objects
progressKeys = 'character',
@@ -363,6 +371,7 @@ ShinySession <- R6Class(
currentOutputName = NULL, # Name of the currently-running output
outputInfo = list(), # List of information for each output
testSnapshotUrl = character(0),
currentThemeDependency = NULL, # ReactiveVal for taking dependency on theme
sendResponse = function(requestMsg, value) {
if (is.null(requestMsg$tag)) {
@@ -468,14 +477,28 @@ ShinySession <- R6Class(
# The format of the response that will be sent back. Defaults to
# "json" unless requested otherwise. The only other valid value is
# "rds".
format <- params$format %OR% "json"
format <- params$format %||% "json"
values <- list()
if (!is.null(params$input)) {
allInputs <- isolate(
reactiveValuesToList(self$input, all.names = TRUE)
# The isolate and reactiveValuesToList calls are being executed
# in a non-reactive context, but will produce output in the reactlog
# Seeing new, unlabelled reactives ONLY when calling shinytest is
# jarring / frustrating to debug.
# Since labeling these values is not currently supported in reactlog,
# it is better to hide them.
# Hopefully we can replace this with something like
# `with_reactlog_group("shinytest", {})`, which would visibily explain
# why the new reactives are added when calling shinytest
withr::with_options(
list(shiny.reactlog = FALSE),
{
allInputs <- isolate(
reactiveValuesToList(self$input, all.names = TRUE)
)
}
)
# If params$input is "1", return all; otherwise return just the
@@ -585,23 +608,22 @@ ShinySession <- R6Class(
# function has been set, return the identity function.
getSnapshotPreprocessOutput = function(name) {
fun <- attr(private$.outputs[[name]], "snapshotPreprocess", exact = TRUE)
fun %OR% identity
fun %||% identity
},
# Get the snapshotPreprocessInput function for an input name. If no preprocess
# function has been set, return the identity function.
getSnapshotPreprocessInput = function(name) {
fun <- private$.input$getMeta(name, "shiny.snapshot.preprocess")
fun %OR% identity
fun %||% identity
},
# See cycleStartAction
startCycle = function() {
# TODO: This should check for busyCount == 0L, and remove the checks from
# the call sites
if (length(private$cycleStartActionQueue) > 0) {
head <- private$cycleStartActionQueue[[1L]]
private$cycleStartActionQueue <- private$cycleStartActionQueue[-1L]
if (private$cycleStartActionQueue$size() > 0) {
head <- private$cycleStartActionQueue$remove()
# After we execute the current cycleStartAction (head), there may be
# more items left on the queue. If the current busyCount > 0, then that
@@ -620,7 +642,7 @@ ShinySession <- R6Class(
# busyCount, it's possible we're calling startCycle spuriously; that's
# OK, it's essentially a no-op in that case.
on.exit({
if (private$busyCount == 0L && length(private$cycleStartActionQueue) > 0L) {
if (private$busyCount == 0L && private$cycleStartActionQueue$size() > 0L) {
later::later(function() {
if (private$busyCount == 0L) {
private$startCycle()
@@ -651,12 +673,15 @@ ShinySession <- R6Class(
cache = NULL, # A cache object used in the session
user = NULL,
groups = NULL,
options = NULL, # For session-specific shinyOptions()
initialize = function(websocket) {
private$websocket <- websocket
self$closed <- FALSE
# TODO: Put file upload context in user/app-specific dir if possible
private$inputMessageQueue <- fastmap::fastqueue()
private$cycleStartActionQueue <- fastmap::fastqueue()
private$invalidatedOutputValues <- Map$new()
private$invalidatedOutputErrors <- Map$new()
private$fileUploadContext <- FileUploadContext$new()
@@ -667,7 +692,7 @@ ShinySession <- R6Class(
private$.input <- ReactiveValues$new(dedupe = FALSE, label = "input")
private$.clientData <- ReactiveValues$new(dedupe = TRUE, label = "clientData")
private$timingRecorder <- ShinyServerTimingRecorder$new()
self$progressStack <- Stack$new()
self$progressStack <- fastmap::faststack()
self$files <- Map$new()
self$downloads <- Map$new()
self$userData <- new.env(parent = emptyenv())
@@ -681,16 +706,26 @@ ShinySession <- R6Class(
private$.outputs <- list()
private$.outputOptions <- list()
self$cache <- MemoryCache$new()
# Copy app-level options
self$options <- getCurrentAppState()$options
self$cache <- cachem::cache_mem(max_size = 200 * 1024^2)
private$bookmarkCallbacks <- Callbacks$new()
private$bookmarkedCallbacks <- Callbacks$new()
private$restoreCallbacks <- Callbacks$new()
private$restoredCallbacks <- Callbacks$new()
private$testMode <- .globals$testMode
private$testMode <- getShinyOption("testmode", default = FALSE)
private$enableTestSnapshot()
# This `withReactiveDomain` is used only to satisfy the reactlog, so that
# it knows to scope this reactiveVal to this session.
# https://github.com/rstudio/shiny/pull/3182
withReactiveDomain(self,
private$currentThemeDependency <- reactiveVal(0, label = "Theme Counter")
)
private$registerSessionEndCallbacks()
if (!is.null(websocket$request$HTTP_SHINY_SERVER_CREDENTIALS)) {
@@ -723,6 +758,12 @@ ShinySession <- R6Class(
requestFlush = function() {
appsNeedingFlush$set(self$token, self)
},
.scheduleTask = function(millis, callback) {
scheduleTask(millis, callback)
},
.now = function(){
getTimeMs()
},
rootScope = function() {
self
},
@@ -922,7 +963,33 @@ ShinySession <- R6Class(
impl <- .subset2(x, 'impl')
key <- .subset2(x, 'ns')(name)
impl$freeze(key)
is_input <- identical(impl, private$.input)
# There's no good reason for us not to just do force=TRUE, except that we
# know this fixes problems for freezeReactiveValue(input) but we don't
# currently even know what you would use freezeReactiveValue(rv) for. In
# the spirit of not breaking things we don't understand, we're making as
# targeted a fix as possible, while emitting a deprecation warning (below)
# that should help us gather more data about the other case.
impl$freeze(key, invalidate = is_input)
if (is_input) {
# Notify the client that this input was frozen. The client will ensure
# that the next time it sees a value for that input, even if the value
# has not changed from the last known value of that input, it will be
# sent to the server anyway.
private$sendMessage(frozen = list(
ids = list(key)
))
} else {
if (getOption("shiny.deprecation.messages", TRUE) && getOption("shiny.deprecation.messages.freeze", TRUE)) {
rlang::warn(
"Support for calling freezeReactiveValue() with non-`input` reactiveValues objects is soft-deprecated, and may be removed in a future version of Shiny. (See https://github.com/rstudio/shiny/issues/3063)",
.frequency = "once", .frequency_id = "freezeReactiveValue")
}
}
self$onFlushed(function() impl$thaw(key))
},
@@ -956,7 +1023,9 @@ ShinySession <- R6Class(
output$suspend()
}
# ..stacktraceon matches with the top-level ..stacktraceoff..
private$closedCallbacks$invoke(onError = printError, ..stacktraceon = TRUE)
withReactiveDomain(self, {
private$closedCallbacks$invoke(onError = printError, ..stacktraceon = TRUE)
})
},
isClosed = function() {
return(self$closed)
@@ -1109,7 +1178,10 @@ ShinySession <- R6Class(
private$.outputOptions[[name]] <- list()
}
else {
stop(paste("Unexpected", class(func), "output for", name))
rlang::abort(c(
paste0("Unexpected ", class(func)[[1]], " object for output$", name),
i = "Did you forget to use a render function?"
))
}
},
getOutput = function(name) {
@@ -1139,7 +1211,7 @@ ShinySession <- R6Class(
length(private$progressKeys) != 0 ||
length(private$invalidatedOutputValues) != 0 ||
length(private$invalidatedOutputErrors) != 0 ||
length(private$inputMessageQueue) != 0
private$inputMessageQueue$size() != 0
)
}
@@ -1171,8 +1243,8 @@ ShinySession <- R6Class(
private$invalidatedOutputValues <- Map$new()
errors <- as.list(private$invalidatedOutputErrors)
private$invalidatedOutputErrors <- Map$new()
inputMessages <- private$inputMessageQueue
private$inputMessageQueue <- list()
inputMessages <- private$inputMessageQueue$as_list()
private$inputMessageQueue$reset()
if (isTRUE(private$testMode)) {
private$storeOutputValues(mergeVectors(values, errors))
@@ -1190,7 +1262,7 @@ ShinySession <- R6Class(
# does not guarantee) inputs and reactive values from changing underneath
# async observers as they run.
cycleStartAction = function(callback) {
private$cycleStartActionQueue <- c(private$cycleStartActionQueue, list(callback))
private$cycleStartActionQueue$add(callback)
# If no observers are running in this session, we're safe to proceed.
# Otherwise, startCycle() will be called later, via decrementBusyCount().
if (private$busyCount == 0L) {
@@ -1230,6 +1302,66 @@ ShinySession <- R6Class(
modal = list(type = type, message = message)
)
},
getCurrentTheme = function() {
private$currentThemeDependency()
getCurrentTheme()
},
setCurrentTheme = function(theme) {
# This function does three things: (1) sets theme as the current
# bootstrapTheme, (2) re-executes any registered theme dependencies, and
# (3) sends the resulting dependencies to the client.
if (!is_bs_theme(theme)) {
stop("`session$setCurrentTheme()` expects a `bslib::bs_theme()` object.", call. = FALSE)
}
# Switching Bootstrap versions has weird & complex consequences
# for the JS logic, so we forbid it
current_version <- bslib::theme_version(getCurrentTheme())
next_version <- bslib::theme_version(theme)
if (!identical(current_version, next_version)) {
stop(
"session$setCurrentTheme() cannot be used to change the Bootstrap version ",
"from ", current_version, " to ", next_version, ". ",
"Try using `bs_theme(version = ", next_version, ")` for initial theme.",
call. = FALSE
)
}
# Note that this will automatically scope to the session.
setCurrentTheme(theme)
# Invalidate
private$currentThemeDependency(isolate(private$currentThemeDependency()) + 1)
# Call any theme dependency functions and make sure we get a list of deps back
funcs <- getShinyOption("themeDependencyFuncs", default = list())
deps <- lapply(funcs, function(func) {
deps <- func(theme)
if (length(deps) == 0) return(NULL)
if (inherits(deps, "html_dependency")) return(list(deps))
is_dep <- vapply(deps, inherits, logical(1), "html_dependency")
if (all(is_dep)) return(deps)
stop("All registerThemeDependency() functions must yield htmlDependency() object(s)", call. = FALSE)
})
# Work with a flat list of dependencies
deps <- unlist(dropNulls(deps), recursive = FALSE)
# Add a special flag to let Shiny.renderDependencies() know that, even
# though we've already rendered the dependency, that we need to re-render
# the stylesheets
deps <- lapply(deps, function(dep) {
dep$restyle <- TRUE
dep
})
# Send any dependencies to be re-rendered
if (length(deps)) {
insertUI(selector = "body", where = "afterEnd", ui = tagList(deps))
}
},
dispatch = function(msg) {
method <- paste('@', msg$method, sep='')
func <- try(self[[method]], silent = TRUE)
@@ -1261,8 +1393,7 @@ ShinySession <- R6Class(
sendInputMessage = function(inputId, message) {
data <- list(id = inputId, message = message)
# Add to input message queue
private$inputMessageQueue[[length(private$inputMessageQueue) + 1]] <- data
private$inputMessageQueue$add(data)
# Needed so that Shiny knows to actually flush the input message queue
self$requestFlush()
},
@@ -1291,40 +1422,101 @@ ShinySession <- R6Class(
getCurrentOutputInfo = function() {
name <- private$currentOutputName
if (is.null(name)) {
return(NULL)
}
tmp_info <- private$outputInfo[[name]] %OR% list(name = name)
if (!is.null(private$outputInfo[[name]])) {
return(private$outputInfo[[name]])
}
# The following code will only run the first time this function has been
# called for this output.
tmp_info <- list(name = name)
# cd_names() returns names of all items in clientData, without taking a
# reactive dependency. It is a function and it's memoized, so that we do
# the (relatively) expensive isolate(names(...)) call only when needed,
# and at most one time in this function.
.cd_names <- NULL
cd_names <- function() {
if (is.null(.cd_names)) {
.cd_names <<- isolate(names(self$clientData))
}
.cd_names
cd_names <- isolate(names(self$clientData))
# parseCssColors() currently errors out if you hand it any NAs
# This'll make sure we're always working with a string (and if
# that string isn't a valid CSS color, will return NA)
# https://github.com/rstudio/htmltools/issues/161
parse_css_colors <- function(x) {
htmltools::parseCssColors(x %||% "", mustWork = FALSE)
}
# If we don't already have width for this output info, see if it's
# present, and if so, add it.
if (! ("width" %in% names(tmp_info)) ) {
width_name <- paste0("output_", name, "_width")
if (width_name %in% cd_names()) {
tmp_info$width <- reactive({
self$clientData[[width_name]]
})
# This function conditionally adds an item to tmp_info (for "width", it
# would create tmp_info$width). It is added _if_ there is an entry in
# clientData like "output_foo_width", where "foo" is the name of the
# output. The first time `tmp_info$width()` is called, it creates a
# reactive expression that reads `clientData$output_foo_width`, saves it,
# then invokes that reactive. On subsequent calls, the reactive already
# exists, so it simply invokes it.
#
# The reason it creates the reactive only on first use is so that it
# doesn't spuriously create reactives.
#
# This function essentially generalizes the code below for names other
# than just "width".
#
# width_name <- paste0("output_", name, "_width")
# if (width_name %in% cd_names()) {
# width_r <- NULL
# tmp_info$width <- function() {
# if (is.null(width_r)) {
# width_r <<- reactive({
# parse_css_colors(self$clientData[[width_name]])
# })
# }
#
# width_r()
# }
# }
add_conditional_reactive <- function(prop, wrapfun = identity) {
force(prop)
force(wrapfun)
prop_name <- paste0("output_", name, "_", prop)
# Only add tmp_info$width if clientData has "output_foo_width"
if (prop_name %in% cd_names) {
r <- NULL
# Turn it into a function that creates a reactive on the first
# invocation of getCurrentOutputInfo()$width() and saves it; future
# invocations of getCurrentOutputInfo()$width() use the existing
# reactive and save it.
tmp_info[[prop]] <<- function() {
if (is.null(r)) {
r <<- reactive(label = prop_name, {
wrapfun(self$clientData[[prop_name]])
})
}
r()
}
}
}
if (! ("height" %in% names(tmp_info)) ) {
height_name <- paste0("output_", name, "_height")
if (height_name %in% cd_names()) {
tmp_info$height <- reactive({
self$clientData[[height_name]]
})
}
}
# Note that all the following clientData values (which are reactiveValues)
# are wrapped in reactive() so that users can take a dependency on
# particular output info (i.e., just depend on width/height, or just
# depend on bg, fg, etc). To put it another way, if getCurrentOutputInfo()
# simply returned a list of values from self$clientData, than anything
# that calls getCurrentOutputInfo() would take a reactive dependency on
# all of these values.
add_conditional_reactive("width")
add_conditional_reactive("height")
add_conditional_reactive("bg", parse_css_colors)
add_conditional_reactive("fg", parse_css_colors)
add_conditional_reactive("accent", parse_css_colors)
add_conditional_reactive("font")
private$outputInfo[[name]] <- tmp_info
private$outputInfo[[name]]
@@ -1341,7 +1533,7 @@ ShinySession <- R6Class(
# Warn if trying to enable save-to-server bookmarking on a version of SS,
# SSP, or Connect that doesn't support it.
if (store == "server" && inShinyServer() &&
is.null(getShinyOption("save.interface")))
is.null(getShinyOption("save.interface", default = NULL)))
{
showNotification(
"This app tried to enable saved-to-server bookmarking, but it is not supported by the hosting environment.",
@@ -1617,10 +1809,6 @@ ShinySession <- R6Class(
)
},
# Public RPC methods
`@uploadieFinish` = function() {
# Do nothing; just want the side effect of flushReact, output flush, etc.
},
`@uploadInit` = function(fileInfos) {
maxSize <- getOption('shiny.maxRequestSize', 5 * 1024 * 1024)
fileInfos <- lapply(fileInfos, function(fi) {
@@ -1687,33 +1875,6 @@ ShinySession <- R6Class(
}
}
# @description Only applicable to files uploaded via IE. When possible,
# adds the appropriate extension to temporary files created by
# \code{mime::parse_multipart}.
# @param multipart A named list as returned by
# \code{mime::parse_multipart}
# @return A named list with datapath updated to point to the new location
# of the file, if an extension was added.
maybeMoveIEUpload <- function(multipart) {
if (is.null(multipart)) return(NULL)
lapply(multipart, function(input) {
oldPath <- input$datapath
newPath <- paste0(oldPath, maybeGetExtension(input$name))
if (oldPath != newPath) {
file.rename(oldPath, newPath)
input$datapath <- newPath
}
input
})
}
if (matches[2] == 'uploadie' && identical(req$REQUEST_METHOD, "POST")) {
id <- URLdecode(matches[3])
res <- maybeMoveIEUpload(mime::parse_multipart(req))
private$.input$set(id, res[[id]])
return(httpResponse(200, 'text/plain', 'OK'))
}
if (matches[2] == 'download') {
@@ -1788,15 +1949,17 @@ ShinySession <- R6Class(
}
return(httpResponse(
200,
download$contentType %OR% getContentType(filename),
download$contentType %||% getContentType(filename),
# owned=TRUE means tmpdata will be deleted after response completes
list(file=tmpdata, owned=TRUE),
c(
'Content-Disposition' = ifelse(
dlmatches[3] == '',
'attachment; filename="' %.%
gsub('(["\\\\])', '\\\\\\1', filename) %.% # yes, that many \'s
'"',
paste0(
'attachment; filename="',
gsub('(["\\\\])', '\\\\\\1', filename),
'"'
),
'attachment'
),
'Cache-Control'='no-cache')))
@@ -1822,33 +1985,18 @@ ShinySession <- R6Class(
return(httpResponse(404, 'text/html', '<h1>Not Found</h1>'))
},
saveFileUrl = function(name, data, contentType, extra=list()) {
"Creates an entry in the file map for the data, and returns a URL pointing
to the file."
self$files$set(name, list(data=data, contentType=contentType))
return(sprintf('session/%s/file/%s?w=%s&r=%s',
URLencode(self$token, TRUE),
URLencode(name, TRUE),
workerId(),
createUniqueId(8)))
},
# Send a file to the client
fileUrl = function(name, file, contentType='application/octet-stream') {
"Return a URL for a file to be sent to the client. If allowDataUriScheme
is TRUE, then the file will be base64 encoded and embedded in the URL.
Otherwise, a URL pointing to the file will be returned."
"Return a URL for a file to be sent to the client. The file will be base64
encoded and embedded in the URL."
bytes <- file.info(file)$size
if (is.na(bytes))
return(NULL)
fileData <- readBin(file, 'raw', n=bytes)
if (isTRUE(private$.clientData$.values$get("allowDataUriScheme"))) {
b64 <- rawToBase64(fileData)
return(paste('data:', contentType, ';base64,', b64, sep=''))
} else {
return(self$saveFileUrl(name, fileData, contentType))
}
b64 <- rawToBase64(fileData)
return(paste('data:', contentType, ';base64,', b64, sep=''))
},
registerDownload = function(name, filename, contentType, func) {
@@ -1983,9 +2131,11 @@ ShinySession <- R6Class(
active = list(
session = function() {
shinyDeprecated(
msg = paste("Attempted to access deprecated shinysession$session object.",
"Please just access the shinysession object directly."),
version = "0.11.1"
"0.11.1", "shinysession$session",
details = paste0(
"Attempted to access deprecated shinysession$session object. ",
"Please just access the shinysession object directly."
)
)
self
}
@@ -2025,7 +2175,7 @@ ShinySession <- R6Class(
if (getOption("shiny.allowoutputreads", FALSE)) {
.subset2(x, 'impl')$getOutput(name)
} else {
stop("Reading from shinyoutput object is not allowed.")
rlang::abort(paste0("Can't read output '", output, "'"))
}
}
@@ -2034,12 +2184,12 @@ ShinySession <- R6Class(
#' @export
`[.shinyoutput` <- function(values, name) {
stop("Single-bracket indexing of shinyoutput object is not allowed.")
rlang::abort("Can't index shinyoutput with `[`.")
}
#' @export
`[<-.shinyoutput` <- function(values, name, value) {
stop("Single-bracket indexing of shinyoutput object is not allowed.")
rlang::abort("Can't index shinyoutput with `[[`.")
}
#' Set options for an output object.
@@ -2088,12 +2238,69 @@ outputOptions <- function(x, name, ...) {
}
#' Get information about the output that is currently being executed.
#' Get output information
#'
#' Returns information about the currently executing output, including its `name` (i.e., `outputId`);
#' and in some cases, relevant sizing and styling information.
#'
#' @param session The current Shiny session.
#'
#' @return `NULL` if called outside of an output context; otherwise,
#' a list which includes:
#' * The `name` of the output (reported for any output).
#' * If the output is a `plotOutput()` or `imageOutput()`, then:
#' * `height`: a reactive expression which returns the height in pixels.
#' * `width`: a reactive expression which returns the width in pixels.
#' * If the output is a `plotOutput()`, `imageOutput()`, or contains a `shiny-report-theme` class, then:
#' * `bg`: a reactive expression which returns the background color.
#' * `fg`: a reactive expression which returns the foreground color.
#' * `accent`: a reactive expression which returns the hyperlink color.
#' * `font`: a reactive expression which returns a list of font information, including:
#' * `families`: a character vector containing the CSS `font-family` property.
#' * `size`: a character string containing the CSS `font-size` property
#'
#' @export
#' @examples
#'
#' if (interactive()) {
#' shinyApp(
#' fluidPage(
#' tags$style(HTML("body {background-color: black; color: white; }")),
#' tags$style(HTML("body a {color: purple}")),
#' tags$style(HTML("#info {background-color: teal; color: orange; }")),
#' plotOutput("p"),
#' "Computed CSS styles for the output named info:",
#' tagAppendAttributes(
#' textOutput("info"),
#' class = "shiny-report-theme"
#' )
#' ),
#' function(input, output) {
#' output$p <- renderPlot({
#' info <- getCurrentOutputInfo()
#' par(bg = info$bg(), fg = info$fg(), col.axis = info$fg(), col.main = info$fg())
#' plot(1:10, col = info$accent(), pch = 19)
#' title("A simple R plot that uses its CSS styling")
#' })
#' output$info <- renderText({
#' info <- getCurrentOutputInfo()
#' jsonlite::toJSON(
#' list(
#' bg = info$bg(),
#' fg = info$fg(),
#' accent = info$accent(),
#' font = info$font()
#' ),
#' auto_unbox = TRUE
#' )
#' })
#' }
#' )
#' }
#'
#'
getCurrentOutputInfo <- function(session = getDefaultReactiveDomain()) {
if (is.null(session)) return(NULL)
session$getCurrentOutputInfo()
}
@@ -2281,3 +2488,72 @@ ShinyServerTimingRecorder <- R6Class("ShinyServerTimingRecorder",
)
missingOutput <- function(...) req(FALSE)
#' Insert inline Markdown
#'
#' This function accepts
#' [Markdown](https://en.wikipedia.org/wiki/Markdown)-syntax text and returns
#' HTML that may be included in Shiny UIs.
#'
#' Leading whitespace is trimmed from Markdown text with [glue::trim()].
#' Whitespace trimming ensures Markdown is processed correctly even when the
#' call to `markdown()` is indented within surrounding R code.
#'
#' By default, [Github extensions][commonmark::extensions] are enabled, but this
#' can be disabled by passing `extensions = FALSE`.
#'
#' Markdown rendering is performed by [commonmark::markdown_html()]. Additional
#' arguments to `markdown()` are passed as arguments to `markdown_html()`
#'
#' @param mds A character vector of Markdown source to convert to HTML. If the
#' vector has more than one element, a single-element character vector of
#' concatenated HTML is returned.
#' @param extensions Enable Github syntax extensions; defaults to `TRUE`.
#' @param .noWS Character vector used to omit some of the whitespace that would
#' normally be written around generated HTML. Valid options include `before`,
#' `after`, and `outside` (equivalent to `before` and `end`).
#' @param ... Additional arguments to pass to [commonmark::markdown_html()].
#' These arguments are _[dynamic][rlang::dyn-dots]_.
#'
#' @return a character vector marked as HTML.
#' @export
#' @examples
#' ui <- fluidPage(
#' markdown("
#' # Markdown Example
#'
#' This is a markdown paragraph, and will be contained within a `<p>` tag
#' in the UI.
#'
#' The following is an unordered list, which will be represented in the UI as
#' a `<ul>` with `<li>` children:
#'
#' * a bullet
#' * another
#'
#' [Links](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) work;
#' so does *emphasis*.
#'
#' To see more of what's possible, check out [commonmark.org/help](https://commonmark.org/help).
#' ")
#' )
markdown <- function(mds, extensions = TRUE, .noWS = NULL, ...) {
html <- rlang::exec(commonmark::markdown_html, glue::trim(mds), extensions = extensions, ...)
htmltools::HTML(html, .noWS = .noWS)
}
# Check that an object is a ShinySession object, and give an informative error.
# The default label is the caller function's name.
validate_session_object <- function(session, label = as.character(sys.call(sys.parent())[[1]])) {
if (missing(session) ||
!inherits(session, c("ShinySession", "MockShinySession", "session_proxy")))
{
stop(call. = FALSE,
sprintf(
"`session` must be a 'ShinySession' object. Did you forget to pass `session` to `%s()`?",
label
)
)
}
}

View File

@@ -13,7 +13,10 @@
#' object to `print()` or [runApp()].
#'
#' @param ui The UI definition of the app (for example, a call to
#' `fluidPage()` with nested controls)
#' `fluidPage()` with nested controls).
#'
#' If bookmarking is enabled (see `enableBookmarking`), this must be
#' a single argument function that returns the UI definition.
#' @param server A function with three parameters: `input`, `output`, and
#' `session`. The function is called once for each session ensuring that each
#' app is independent.
@@ -30,11 +33,9 @@
#' request. Note that the entire request path must match the regular
#' expression in order for the match to be considered successful.
#' @param enableBookmarking Can be one of `"url"`, `"server"`, or
#' `"disable"`. This is equivalent to calling the
#' [enableBookmarking()] function just before calling
#' `shinyApp()`. With the default value (`NULL`), the app will
#' respect the setting from any previous calls to `enableBookmarking()`.
#' See [enableBookmarking()] for more information.
#' `"disable"`. The default value, `NULL`, will respect the setting from
#' any previous calls to [enableBookmarking()]. See [enableBookmarking()]
#' for more information on bookmarking your app.
#' @return An object that represents the app. Printing the object or passing it
#' to [runApp()] will run the app.
#'
@@ -92,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(
@@ -113,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
@@ -125,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"
)
}
}
@@ -146,15 +152,13 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
# Most of the complexity here comes from needing to hot-reload if the .R files
# change on disk, or are created, or are removed.
# In an upcoming version of shiny, this option will go away and the new behavior will be used.
if (getOption("shiny.autoload.r", FALSE)) {
# new behavior
# 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.
sharedEnv <- new.env(parent = globalenv())
} else {
# old behavior, default
# old behavior
sharedEnv <- globalenv()
}
@@ -228,19 +232,24 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
onStart <- function() {
oldwd <<- getwd()
setwd(appDir)
monitorHandle <<- initAutoReloadMonitor(appDir)
# TODO: we should support hot reloading on global.R and R/*.R changes.
if (getOption("shiny.autoload.r", FALSE)) {
if (getOption("shiny.autoload.r", TRUE)) {
loadSupport(appDir, renv=sharedEnv, globalrenv=globalenv())
} else {
if (file.exists(file.path.ci(appDir, "global.R")))
sourceUTF8(file.path.ci(appDir, "global.R"))
}
monitorHandle <<- initAutoReloadMonitor(appDir)
}
onStop <- function() {
setwd(oldwd)
monitorHandle()
monitorHandle <<- NULL
# It is possible that while calling appObj()$onStart() or loadingSupport, an error occured
# This will cause `onStop` to be called.
# The `oldwd` will exist, but `monitorHandle` is not a function yet.
if (is.function(monitorHandle)) {
monitorHandle()
monitorHandle <<- NULL
}
}
structure(
@@ -285,9 +294,11 @@ initAutoReloadMonitor <- function(dir) {
".*\\.(r|html?|js|css|png|jpe?g|gif)$")
lastValue <- NULL
obs <- observe({
files <- sort(list.files(dir, pattern = filePattern, recursive = TRUE,
ignore.case = TRUE))
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
@@ -297,14 +308,14 @@ initAutoReloadMonitor <- function(dir) {
} else if (!identical(lastValue, times)) {
# We've changed!
lastValue <<- times
for (session in appsByToken$values()) {
session$reload()
}
autoReloadCallbacks$invoke()
}
invalidateLater(getOption("shiny.autoreload.interval", 500))
})
onStop(obs$destroy)
obs$destroy
}
@@ -314,30 +325,71 @@ initAutoReloadMonitor <- function(dir) {
#' this function loads any top-level supporting `.R` files in the `R/` directory
#' adjacent to the `app.R`/`server.R`/`ui.R` files.
#'
#' At the moment, this function is "opt-in" and only called if the option
#' `shiny.autoload.r` is set to `TRUE`.
#' Since Shiny 1.5.0, this function is called by default when running an
#' application. If it causes problems, there are two ways to opt out. You can
#' either place a file named `_disable_autoload.R` in your R/ directory, or
#' set `options(shiny.autoload.r=FALSE)`. If you set this option, it will
#' affect any application that runs later in the same R session, potentially
#' breaking it, so after running your application, you should unset option with
#' `options(shiny.autoload.r=NULL)`
#'
#' @details The files are sourced in alphabetical order (as determined by
#' [list.files]). `global.R` is evaluated before the supporting R files in the
#' `R/` directory.
#' @param appDir The application directory
#' @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
#' be evaluated.
#' @param globalrenv The environment in which `global.R` should be evaluated. If
#' `NULL`, `global.R` will not be evaluated at all.
#' @export
loadSupport <- function(appDir, renv=new.env(parent=globalenv()), globalrenv=globalenv()){
loadSupport <- function(appDir=NULL, renv=new.env(parent=globalenv()), globalrenv=globalenv()){
require(shiny)
if (is.null(appDir)) {
appDir <- findEnclosingApp(".")
}
descFile <- file.path.ci(appDir, "DESCRIPTION")
if (file.exists(file.path.ci(appDir, "NAMESPACE")) ||
(file.exists(descFile) &&
identical(as.character(read.dcf(descFile, fields = "Type")), "Package")))
{
warning(
"Loading R/ subdirectory for Shiny application, but this directory appears ",
"to contain an R package. Sourcing files in R/ may cause unexpected behavior."
)
}
if (!is.null(globalrenv)){
# Evaluate global.R, if it exists.
if (file.exists(file.path.ci(appDir, "global.R"))){
sourceUTF8(file.path.ci(appDir, "global.R"), envir=globalrenv)
globalPath <- file.path.ci(appDir, "global.R")
if (file.exists(globalPath)){
withr::with_dir(appDir, {
sourceUTF8(basename(globalPath), envir=globalrenv)
})
}
}
helpersDir <- file.path(appDir, "R")
helpers <- list.files(helpersDir, pattern="\\.[rR]$", recursive=FALSE, full.names=TRUE)
lapply(helpers, sourceUTF8, envir=renv)
helpersDir <- file.path(appDir, "R")
disabled <- list.files(helpersDir, pattern="^_disable_autoload\\.r$", recursive=FALSE, ignore.case=TRUE)
if (length(disabled) > 0){
return(invisible(renv))
}
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:
# https://cran.r-project.org/doc/manuals/r-release/R-exts.html#The-DESCRIPTION-file
helpers <- sort_c(helpers)
helpers <- normalizePath(helpers)
withr::with_dir(appDir, {
lapply(helpers, sourceUTF8, envir=renv)
})
invisible(renv)
}
@@ -349,30 +401,30 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
{
fullpath <- file.path.ci(appDir, fileName)
# In an upcoming version of shiny, this option will go away and the new behavior will be used.
if (getOption("shiny.autoload.r", FALSE)) {
# new behavior
# Create a child env which contains all the helpers and will be the shared parent
# of the ui.R and server.R load.
sharedEnv <- new.env(parent = globalenv())
} else {
# old behavior, default
sharedEnv <- globalenv()
}
# This sources app.R and caches the content. When appObj() is called but
# app.R hasn't changed, it won't re-source the file. But if called and
# app.R has changed, it'll re-source the file and return the result.
appObj <- cachedFuncWithFile(appDir, fileName, case.sensitive = FALSE,
function(appR) {
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.
sharedEnv <- new.env(parent = globalenv())
loadSupport(appDir, renv=sharedEnv, globalrenv=NULL)
} else {
sharedEnv <- globalenv()
}
result <- sourceUTF8(fullpath, envir = new.env(parent = sharedEnv))
if (!is.shiny.appobj(result))
stop("app.R did not return a shiny.appobj object.")
unconsumeAppOptions(result$appOptions)
applyCapturedAppOptions(result$appOptions)
return(result)
}
@@ -410,19 +462,23 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
onStart <- function() {
oldwd <<- getwd()
setwd(appDir)
# TODO: we should support hot reloading on R/*.R changes.
if (getOption("shiny.autoload.r", FALSE)) {
loadSupport(appDir, renv=sharedEnv, globalrenv=NULL)
}
monitorHandle <<- initAutoReloadMonitor(appDir)
if (!is.null(appObj()$onStart)) appObj()$onStart()
monitorHandle <<- initAutoReloadMonitor(appDir)
invisible()
}
onStop <- function() {
setwd(oldwd)
monitorHandle()
monitorHandle <<- NULL
# It is possible that while calling appObj()$onStart() or loadingSupport, an error occured
# This will cause `onStop` to be called.
# The `oldwd` will exist, but `monitorHandle` is not a function yet.
if (is.function(monitorHandle)) {
monitorHandle()
monitorHandle <<- NULL
}
}
appObjOptions <- appObj()$options
structure(
list(
# fallbackWWWDir is _not_ listed in staticPaths, because it needs to
@@ -441,7 +497,7 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
serverFuncSource = dynServerFuncSource,
onStart = onStart,
onStop = onStop,
options = options
options = joinOptions(appObjOptions, options)
),
class = "shiny.appobj"
)
@@ -491,18 +547,25 @@ is.shiny.appobj <- function(x) {
}
#' @rdname shiny.appobj
#' @param ... Additional parameters to be passed to print.
#' @param ... Ignored.
#' @export
print.shiny.appobj <- function(x, ...) {
opts <- x$options %OR% list()
opts <- opts[names(opts) %in%
c("port", "launch.browser", "host", "quiet",
"display.mode", "test.mode")]
runApp(x)
}
# Quote x and put runApp in quotes so that there's a nicer stack trace (#1851)
args <- c(list(quote(x)), opts)
# Joins two options objects (i.e. the `options` argument to shinyApp(),
# shinyAppDir(), etc.). The values in `b` should take precedence over the values
# in `a`. Given the current options available, it is safe to throw away any
# values in `a` that are provided in `b`. But in the future, if new options are
# introduced that need to be combined in some way instead of simply overwritten,
# then this will be the place to do it. See the implementations of
# print.shiny.appobj() and runApp() (for the latter, look specifically for
# "findVal()") to determine the set of possible options.
joinOptions <- function(a, b) {
stopifnot(is.null(a) || is.list(a))
stopifnot(is.null(b) || is.list(b))
do.call("runApp", args)
mergeVectors(a, b)
}
#' @rdname shiny.appobj
@@ -512,7 +575,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
@@ -532,84 +595,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

@@ -24,7 +24,9 @@ withMathJax <- function(...) {
)
}
renderPage <- function(ui, connection, showcase=0, testMode=FALSE) {
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")) {
@@ -38,7 +40,10 @@ renderPage <- function(ui, connection, showcase=0, testMode=FALSE) {
# Put the body into the default template
ui <- htmlTemplate(
system.file("template", "default.html", package = "shiny"),
body = ui
lang = lang,
body = ui,
# this template is a complete HTML document
document_ = TRUE
)
}
@@ -46,7 +51,7 @@ renderPage <- function(ui, connection, showcase=0, testMode=FALSE) {
version <- getOption("shiny.jquery.version", 3)
if (version == 3) {
return(htmlDependency(
"jquery", "3.4.1",
"jquery", version_jquery,
c(href = "shared"),
script = "jquery.min.js"
))
@@ -61,28 +66,73 @@ renderPage <- function(ui, connection, showcase=0, testMode=FALSE) {
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(jquery()),
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", shinyPackageVersion(),
c(href="shared"), script = "shiny-testmode.js")
}
html <- renderDocument(ui, shiny_deps, processDep = createWebDependency)
writeUTF8(html, con = connection)
enc2utf8(paste(collapse = "\n", html))
}
shinyDependencies <- function() {
list(
bslib::bs_dependency_defer(shinyDependencyCSS),
htmlDependency(
name = "shiny-javascript",
version = shinyPackageVersion(),
src = c(href = "shared"),
script =
if (isTRUE(
get_devmode_option(
"shiny.minified",
TRUE
)
))
"shiny.min.js"
else
"shiny.js"
)
)
}
shinyDependencyCSS <- function(theme) {
version <- shinyPackageVersion()
if (!is_bs_theme(theme)) {
return(htmlDependency(
name = "shiny-css",
version = version,
src = c(href = "shared"),
stylesheet = "shiny.min.css"
))
}
scss_home <- system.file("www/shared/shiny_scss", package = "shiny")
scss_files <- file.path(scss_home, c("bootstrap.scss", "shiny.scss"))
scss_files <- lapply(scss_files, sass::sass_file)
bslib::bs_dependency(
input = scss_files,
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 \lifecycle{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
@@ -93,6 +143,17 @@ renderPage <- function(ui, connection, showcase=0, testMode=FALSE) {
#' @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
}
@@ -101,16 +162,18 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
force(ui)
allowed_methods <- "GET"
if (is.function(ui)) {
allowed_methods <- attr(ui, "http_methods_supported", exact = TRUE) %||% allowed_methods
}
function(req) {
if (!identical(req$REQUEST_METHOD, 'GET'))
if (!isTRUE(req$REQUEST_METHOD %in% allowed_methods))
return(NULL)
if (!isTRUE(grepl(uiPattern, req$PATH_INFO)))
return(NULL)
textConn <- file(open = "w+")
on.exit(close(textConn))
showcaseMode <- .globals$showcaseDefault
if (.globals$showcaseOverride) {
mode <- showcaseModeOfReq(req)
@@ -118,7 +181,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")
@@ -150,8 +213,11 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
if (is.null(uiValue))
return(NULL)
renderPage(uiValue, textConn, showcaseMode, testMode)
html <- paste(readLines(textConn, encoding = 'UTF-8'), collapse='\n')
return(httpResponse(200, content=enc2utf8(html)))
if (inherits(uiValue, "httpResponse")) {
return(uiValue)
} else {
html <- renderPage(uiValue, showcaseMode, testMode)
return(httpResponse(200, content=html))
}
}
}

View File

@@ -1,34 +1,105 @@
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.
#' 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.
#'
#' @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()], [quoToFunction()]
#' @export
markRenderFunction <- function(uiFunc, renderFunc, outputArgs = list()) {
markRenderFunction <- function(
uiFunc,
renderFunc,
outputArgs = list(),
cacheHint = "auto",
cacheWriteHook = NULL,
cacheReadHook = NULL
) {
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.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 +112,20 @@ 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)
structure(
wrappedRenderFunc,
class = c("shiny.render.function", "function"),
outputFunc = uiFunc,
outputArgs = outputArgs,
hasExecuted = hasExecuted,
cacheHint = cacheHint,
cacheWriteHook = cacheWriteHook,
cacheReadHook = cacheReadHook
)
}
#' @export
@@ -59,6 +135,9 @@ print.shiny.render.function <- function(x, ...) {
#' Implement render functions
#'
#' This function is a wrapper for [markRenderFunction()] which provides support
#' for async computation via promises.
#'
#' @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
#' mode.
@@ -70,34 +149,63 @@ 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.
#'
#' @seealso [quoToFunction()], [markRenderFunction()].
#'
#' @examples
#' # A very simple render function
#' renderTriple <- function(x) {
#' x <- substitute(x)
#' if (!rlang::is_quosure(x)) {
#' x <- rlang::new_quosure(x, env = parent.frame())
#' }
#' func <- quoToFunction(x, "renderTriple")
#'
#' createRenderFunction(
#' func,
#' transform = function(value, session, name, ...) {
#' paste(rep(value, 3), collapse=", ")
#' },
#' outputFunc = textOutput
#' )
#' }
#'
#' # Test render function from the console
#' a <- 1
#' r <- renderTriple({ a + 1 })
#' a <- 2
#' r()
#' @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.
if (identical(cacheHint, "auto")) {
attr(renderFunc, "wrappedFunc") <- attr(func, "wrappedFunc", exact = TRUE)
}
markRenderFunction(outputFunc, renderFunc, outputArgs, cacheHint,
cacheWriteHook, cacheReadHook)
}
useRenderFunction <- function(renderFunc, inline = FALSE) {
@@ -140,6 +248,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")
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
#'
@@ -198,7 +322,10 @@ markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
#' @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`;
#' if the image is not a temp file, this should be `FALSE`.
#' if the image is not a temp file, this should be `FALSE`. (For backward
#' compatibility reasons, if this argument is missing, a warning will be
#' emitted, and if the file is in the temp directory it will be deleted. In
#' the future, this warning will become an error.)
#' @param outputArgs A list of arguments to be passed through to the implicit
#' call to [imageOutput()] when `renderImage` is used in an
#' interactive R Markdown document.
@@ -271,19 +398,59 @@ markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
#' shinyApp(ui, server)
#' }
renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
deleteFile=TRUE, outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
deleteFile, outputArgs=list())
{
expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderImage")
# missing() must be used directly within the function with the given arg
if (missing(deleteFile)) {
deleteFile <- NULL
}
# Tracks whether we've reported the `deleteFile` warning yet; we don't want to
# do it on every invalidation (though we will end up doing it at least once
# per output per session).
warned <- FALSE
createRenderFunction(func,
transform = function(imageinfo, session, name, ...) {
# Should the file be deleted after being sent? If .deleteFile not set or if
# TRUE, then delete; otherwise don't delete.
if (deleteFile) {
on.exit(unlink(imageinfo$src))
shouldDelete <- deleteFile
# jcheng 2020-05-08
#
# Until Shiny 1.5.0, the default for deleteFile was, incredibly, TRUE.
# Changing it to default to FALSE might cause existing Shiny apps to pile
# up images in their temp directory (for long lived R processes). Not
# having a default (requiring explicit value) is the right long-term move,
# but would break today's apps.
#
# Compromise we decided on was to eventually require TRUE/FALSE, but for
# now, change the default behavior to only delete temp files; and emit a
# warning encouraging people to not rely on the default.
if (is.null(shouldDelete)) {
shouldDelete <- isTRUE(try(silent = TRUE,
file.exists(imageinfo$src) && isTemp(imageinfo$src, mustExist = TRUE)
))
if (!warned) {
warned <<- TRUE
warning("The renderImage output named '",
getCurrentOutputInfo()$name,
"' is missing the deleteFile argument; as of Shiny 1.5.0, you must ",
"use deleteFile=TRUE or deleteFile=FALSE. (This warning will ",
"become an error in a future version of Shiny.)",
call. = FALSE
)
}
}
if (shouldDelete) {
on.exit(unlink(imageinfo$src), add = TRUE)
}
# 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')]
@@ -292,45 +459,85 @@ 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
# fs::path_has_parent().
isTemp <- function(path, tempDir = tempdir(), mustExist) {
if (!isTRUE(mustExist)) {
# jcheng 2020-05-11: I added mustExist just to make it totally obvious that
# the path must exist. We don't support the case where the file doesn't
# exist because it makes normalizePath unusable, and it's a bit scary
# security-wise to compare paths without normalization. Using fs would fix
# this as it knows how to normalize paths that don't exist.
stop("isTemp(mustExist=FALSE) is not implemented")
}
#' Printable Output
if (mustExist && !file.exists(path)) {
stop("path does not exist")
}
if (nchar(tempDir) == 0 || !dir.exists(tempDir)) {
# This should never happen, but just to be super paranoid...
stop("invalid temp dir")
}
path <- normalizePath(path, winslash = "/", mustWork = mustExist)
tempDir <- normalizePath(tempDir, winslash = "/", mustWork = TRUE)
if (path == tempDir) {
return(FALSE)
}
tempDir <- ensure_trailing_slash(tempDir)
if (path == tempDir) {
return(FALSE)
}
return(substr(path, 1, nchar(tempDir)) == tempDir)
}
#' Text Output
#'
#' Makes a reactive version of the given function that captures any printed
#' output, and also captures its printable result (unless
#' [base::invisible()]), into a string. The resulting function is suitable
#' for assigning to an `output` slot.
#' @description
#' `renderPrint()` prints the result of `expr`, while `renderText()` pastes it
#' together into a single string. `renderPrint()` is equivalent to [print()];
#' `renderText()` is equivalent to [cat()]. Both functions capture all other
#' printed output generated while evaluating `expr`.
#'
#' `renderPrint()` is usually paired with [verbatimTextOutput()];
#' `renderText()` is usually paired with [textOutput()].
#'
#' @details
#' The corresponding HTML output tag can be anything (though `pre` is
#' recommended if you need a monospace font and whitespace preserved) and should
#' have the CSS class name `shiny-text-output`.
#'
#' The result of executing `func` will be printed inside a
#' [utils::capture.output()] call.
#' @return
#' For `renderPrint()`, note the given expression returns `NULL` then `NULL`
#' will actually be visible in the output. To display nothing, make your
#' function return [invisible()].
#'
#' Note that unlike most other Shiny output functions, if the given function
#' returns `NULL` then `NULL` will actually be visible in the output.
#' To display nothing, make your function return [base::invisible()].
#'
#' @param expr An expression that may print output and/or return a printable R
#' object.
#' @param env The environment in which to evaluate `expr`.
#' @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.
#' @param width The value for `[options][base::options]('width')`.
#' @param width Width of printed output.
#' @param outputArgs A list of arguments to be passed through to the implicit
#' call to [verbatimTextOutput()] when `renderPrint` is used
#' in an interactive R Markdown document.
#' @seealso [renderText()] for displaying the value returned from a
#' function, instead of the printed output.
#' call to [verbatimTextOutput()] or [textOutput()] when the functions are
#' used in an interactive RMarkdown document.
#'
#' @example res/text-example.R
#' @export
renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
width = getOption('width'), outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
width = getOption('width'), outputArgs=list())
{
expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderPrint")
# Set a promise domain that sets the console width
# and captures output
@@ -343,12 +550,12 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
{
promises::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)
@@ -356,7 +563,15 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
)
}
markRenderFunction(verbatimTextOutput, renderFunc, outputArgs = outputArgs)
markRenderFunction(
verbatimTextOutput,
renderFunc,
outputArgs,
cacheHint = list(
label = "renderPrint",
origUserExpr = get_expr(expr)
)
)
}
createRenderPrintPromiseDomain <- function(width) {
@@ -400,45 +615,23 @@ createRenderPrintPromiseDomain <- function(width) {
)
}
#' Text Output
#'
#' Makes a reactive version of the given function that also uses
#' [base::cat()] to turn its result into a single-element character
#' vector.
#'
#' The corresponding HTML output tag can be anything (though `pre` is
#' recommended if you need a monospace font and whitespace preserved) and should
#' have the CSS class name `shiny-text-output`.
#'
#' The result of executing `func` will passed to `cat`, inside a
#' [utils::capture.output()] call.
#'
#' @param expr An expression that returns an R object that can be used as an
#' argument to `cat`.
#' @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.
#' @param outputArgs A list of arguments to be passed through to the implicit
#' call to [textOutput()] when `renderText` is used in an
#' interactive R Markdown document.
#' @param sep A separator passed to `cat` to be appended after each
#' element.
#'
#' @seealso [renderPrint()] for capturing the print output of a
#' function, rather than the returned text value.
#'
#' @example res/text-example.R
#' @export
#' @rdname renderPrint
renderText <- function(expr, env=parent.frame(), quoted=FALSE,
outputArgs=list(), sep=" ") {
installExprFunction(expr, "func", env, quoted)
expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderText")
createRenderFunction(
func,
function(value, session, name, ...) {
paste(utils::capture.output(cat(value, sep=sep)), collapse="\n")
},
textOutput, outputArgs
textOutput,
outputArgs
)
}
@@ -479,9 +672,11 @@ 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())
{
expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderUI")
createRenderFunction(
func,
@@ -491,7 +686,8 @@ renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
processDeps(result, shinysession)
},
uiOutput, outputArgs
uiOutput,
outputArgs
)
}
@@ -514,7 +710,7 @@ 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
#' [content type](https://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.
@@ -527,7 +723,7 @@ renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
#' if (interactive()) {
#'
#' ui <- fluidPage(
#' downloadLink("downloadData", "Download")
#' downloadButton("downloadData", "Download")
#' )
#'
#' server <- function(input, output) {
@@ -552,32 +748,41 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
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
#'
#' @description
#' 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.
#' matrix), which will be rendered with the [DataTables](https://datatables.net)
#' library. Paging, searching, filtering, and sorting can be done on the R side
#' using Shiny as the server infrastructure.
#'
#' 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
#' [DT](https://github.com/rstudio/DT) that allows you to create both
#' server-side and client-side DataTables, and supports additional features.
#' Learn more at <https://rstudio.github.io/DT/shiny.html>.
#'
#' 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
@@ -585,17 +790,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
@@ -623,8 +819,18 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
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())
{
if (in_devmode()) {
shinyDeprecated(
"0.11.1", "shiny::renderDataTable()", "DT::renderDataTable()",
details = "See <https://rstudio.github.io/DT/shiny.html> for more information"
)
}
expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderDataTable")
renderFunc <- function(shinysession, name, ...) {
if (is.function(options)) options <- options()
@@ -658,7 +864,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
@@ -715,6 +922,9 @@ checkDT9 <- function(options) {
# Deprecated functions ------------------------------------------------------
#' Deprecated reactive functions
#'
#' @description \lifecycle{superseded}
#'
#' @name deprecatedReactives
#' @keywords internal
NULL
@@ -729,7 +939,7 @@ NULL
#' @rdname deprecatedReactives
#' @export
reactivePlot <- function(func, width='auto', height='auto', ...) {
shinyDeprecated(new="renderPlot")
shinyDeprecated("0.4.0", "reactivePlot()", "renderPlot()")
renderPlot({ func() }, width=width, height=height, ...)
}
@@ -739,7 +949,7 @@ reactivePlot <- function(func, width='auto', height='auto', ...) {
#' @rdname deprecatedReactives
#' @export
reactiveTable <- function(func, ...) {
shinyDeprecated(new="renderTable")
shinyDeprecated("0.4.0", "reactiveTable()", "renderTable()")
renderTable({ func() })
}
@@ -749,7 +959,7 @@ reactiveTable <- function(func, ...) {
#' @rdname deprecatedReactives
#' @export
reactivePrint <- function(func) {
shinyDeprecated(new="renderPrint")
shinyDeprecated("0.4.0", "reactivePrint()", "renderPrint()")
renderPrint({ func() })
}
@@ -759,7 +969,7 @@ reactivePrint <- function(func) {
#' @rdname deprecatedReactives
#' @export
reactiveUI <- function(func) {
shinyDeprecated(new="renderUI")
shinyDeprecated("0.4.0", "reactiveUI()", "renderUI()")
renderUI({ func() })
}
@@ -769,6 +979,6 @@ reactiveUI <- function(func) {
#' @rdname deprecatedReactives
#' @export
reactiveText <- function(func) {
shinyDeprecated(new="renderText")
shinyDeprecated("0.4.0", "reactiveText()", "renderText()")
renderText({ func() })
}

View File

@@ -104,20 +104,18 @@ navTabsDropdown <- function(files) {
tabContentHelper <- function(files, path, language) {
lapply(files, function(file) {
with(tags,
div(class=paste("tab-pane",
tags$div(class=paste("tab-pane",
if (tolower(file) %in% c("app.r", "server.r")) " active"
else "",
sep=""),
id=paste(gsub(".", "_", file, fixed=TRUE),
"_code", sep=""),
pre(class="shiny-code",
tags$pre(class="shiny-code",
# we need to prevent the indentation of <code> ... </code>
HTML(format(tags$code(
class=paste0("language-", language),
paste(readUTF8(file.path.ci(path, file)), collapse="\n")
), indent = FALSE))))
)
})
}
@@ -222,4 +220,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
)
)

156
R/test-server.R Normal file
View File

@@ -0,0 +1,156 @@
#' 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 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
#' [rlang::eval_tidy()]). This includes the parameters of the server function
#' (e.g. `input`, `output`, and `session`), along with any other values
#' created inside of the server function.
#' @param args Additional arguments to pass to the module function. If `app` is
#' a module, and no `id` argument is provided, one will be generated and
#' supplied automatically.
#' @param session The [`MockShinySession`] object to use as the [reactive
#' domain][shiny::domains]. The same session object is used as the domain both
#' during invocation of the server or module under test and during evaluation
#' of `expr`.
#' @include mock-session.R
#' @rdname testServer
#' @examples
#' # 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
#' })
#' output$txt <- renderText({
#' paste0(prefix, myreactive())
#' })
#' })
#' }
#'
#' testServer(myModuleServer, args = list(multiplier = 2), {
#' session$setInputs(x = 1)
#' # You're also free to use third-party
#' # testing packages like testthat:
#' # expect_equal(myreactive(), 2)
#' stopifnot(myreactive() == 2)
#' stopifnot(output$txt == "I am 2")
#'
#' session$setInputs(x = 2)
#' stopifnot(myreactive() == 4)
#' stopifnot(output$txt == "I am 4")
#' # Any additional arguments, below, are passed along to the module.
#' })
#' @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)
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(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(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"))
}

189
R/test.R Normal file
View File

@@ -0,0 +1,189 @@
#' Creates and returns run result data frame.
#'
#' @param file Name of the test runner file, a character vector of length 1.
#' @param pass Whether or not the test passed, a logical vector of length 1.
#' @param result Value (wrapped in a list) obtained by evaluating `file`.
#' This can also by any errors signaled when evaluating the `file`.
#'
#' @return A 1-row data frame representing a single test run. `result` and
#' is a "list column", or a column that contains list elements.
#' @noRd
result_row <- function(file, pass, result) {
stopifnot(is.list(result))
df <- data.frame(
file = file,
pass = pass,
result = I(result),
stringsAsFactors = FALSE
)
class(df) <- c("shiny_runtests", class(df))
df
}
#' Check to see if the given directory contains at least one script, and that
#' all scripts in the directory are shinytest scripts.
#' Scans for the magic string of `app <- ShinyDriver$new(` as an indicator that
#' this is a shinytest.
#' @noRd
is_legacy_shinytest_dir <- function(path){
is_shinytest_script <- function(file) {
if (!file.exists(file)) {
return(FALSE)
}
text <- readLines(file, warn = FALSE)
any(
grepl("app\\s*<-\\s*ShinyDriver\\$new\\(", text, perl=TRUE)
)
}
files <- dir(path, full.names = TRUE)
files <- files[!file.info(files)$isdir]
if (length(files) == 0) {
return(FALSE)
}
all(vapply(files, is_shinytest_script, logical(1)))
}
#' Runs the tests associated with this Shiny app
#'
#' Sources the `.R` files in the top-level of `tests/` much like `R CMD check`.
#' These files are typically simple runners for tests nested in other
#' directories under `tests/`.
#'
#' @param appDir The base directory for the application.
#' @param filter If not `NULL`, only tests with file names matching this regular
#' expression will be executed. Matching is performed on the file name
#' including the extension.
#' @param assert Logical value which determines if an error should be thrown if any error is captured.
#' @param envir Parent testing environment in which to base the individual testing environments.
#'
#' @return A data frame classed with the supplemental class `"shiny_runtests"`.
#' The data frame has the following columns:
#'
#' | **Name** | **Type** | **Meaning** |
#' | :-- | :-- | :-- |
#' | `file` | `character(1)` | File name of the runner script in `tests/` that was sourced. |
#' | `pass` | `logical(1)` | Whether or not the runner script signaled an error when sourced. |
#' | `result` | any or `NA` | The return value of the runner |
#'
#' @details Historically, [shinytest](https://rstudio.github.io/shinytest/)
#' recommended placing tests at the top-level of the `tests/` directory.
#' This older folder structure is not supported by runTests.
#' Please see [shinyAppTemplate()] for more details.
#' @export
runTests <- function(
appDir = ".",
filter = NULL,
assert = TRUE,
envir = globalenv()
) {
# similar to runApp()
# Allows shiny's functions to be available in the UI, server, and test code
require(shiny)
testsDir <- file.path(appDir, "tests")
if (!dirExists(testsDir)) {
stop("No tests directory found: ", testsDir)
}
runners <- list.files(testsDir, pattern="\\.r$", ignore.case = TRUE)
if (length(runners) == 0) {
message("No test runners found in ", testsDir)
return(result_row(character(0), logical(0), list()))
}
if (!is.null(filter)) {
runners <- runners[grepl(filter, runners)]
}
if (length(runners) == 0) {
stop("No test runners matched the given filter: '", filter, "'")
}
# See the @details section of the runTests() docs above for why this branch exists.
if (is_legacy_shinytest_dir(testsDir)) {
stop(
"It appears that the .R files in ", testsDir, " are all shinytests.",
" This is not supported by `shiny::runTests()`.",
"\nPlease see `?shinytest::migrateShinytestDir` to migrate your shinytest file structure to the new format (requires shinytest 1.4.0 or above).",
"\nSee `?shiny::shinyAppTemplate` for an example of the new testing file structure."
)
}
renv <- new.env(parent = envir)
# Otherwise source all the runners -- each in their own environment.
ret <- do.call(rbind, lapply(runners, function(r) {
pass <- FALSE
result <-
tryCatch({
env <- new.env(parent = renv)
withr::with_dir(testsDir, {
ret <- sourceUTF8(r, envir = env)
})
pass <- TRUE
ret
}, error = function(err) {
message("Error in ", r, "\n", err)
err
})
result_row(file.path(testsDir, r), pass, list(result))
}))
if (isTRUE(assert)) {
if (!all(ret$pass)) {
stop("Shiny App Test Failures detected in\n", paste0("* ", runtest_pretty_file(ret$file[!ret$pass]), collapse = "\n"), call. = FALSE)
}
}
ret
}
runtest_pretty_file <- function(f) {
test_folder <- dirname(f)
app_folder <- dirname(test_folder)
file.path(
basename(app_folder),
basename(test_folder),
basename(f)
)
}
#' @export
print.shiny_runtests <- function(x, ..., reporter = "summary") {
cat("Shiny App Test Results\n")
if (any(x$pass)) {
# TODO in future... use clisymbols::symbol$tick and crayon green
cat("* Success\n")
mapply(
x$file,
x$pass,
x$result,
FUN = function(file, pass, result) {
if (!pass) return()
# print(result)
cat(" - ", runtest_pretty_file(file), "\n", sep = "")
}
)
}
if (any(!x$pass)) {
# TODO in future... use clisymbols::symbol$cross and crayon red
cat("* Failure\n")
mapply(
x$file,
x$pass,
x$result,
FUN = function(file, pass, result) {
if (pass) return()
cat(" - ", runtest_pretty_file(file), "\n", sep = "")
}
)
}
invisible(x)
}

View File

@@ -1,6 +1,5 @@
# Return the current time, in milliseconds from epoch, with
# unspecified time zone.
now <- function() {
# Return the current time, in milliseconds from epoch.
getTimeMs <- function() {
as.numeric(Sys.time()) * 1000
}
@@ -12,9 +11,11 @@ TimerCallbacks <- R6Class(
.nextId = 0L,
.funcs = 'Map',
.times = data.frame(),
.now = 'Function',
initialize = function() {
initialize = function(nowFn = getTimeMs) {
.funcs <<- Map$new()
.now <<- nowFn
},
clear = function() {
.nextId <<- 0L
@@ -30,7 +31,7 @@ TimerCallbacks <- R6Class(
id <- .nextId
.nextId <<- .nextId + 1L
t <- now()
t <- .now()
# TODO: Horribly inefficient, use a heap instead
.times <<- rbind(.times, data.frame(time=t+millis,
@@ -56,17 +57,17 @@ TimerCallbacks <- R6Class(
timeToNextEvent = function() {
if (dim(.times)[1] == 0)
return(Inf)
return(.times[1, 'time'] - now())
return(.times[1, 'time'] - .now())
},
takeElapsed = function() {
t <- now()
elapsed <- .times$time < now()
t <- .now()
elapsed <- .times$time <= .now()
result <- .times[elapsed,]
.times <<- .times[!elapsed,]
# TODO: Examine scheduled column to check if any funny business
# has occurred with the system clock (e.g. if scheduled
# is later than now())
# is later than .now())
return(result)
},
@@ -86,6 +87,30 @@ TimerCallbacks <- R6Class(
)
)
MockableTimerCallbacks <- R6Class(
'MockableTimerCallbacks',
inherit = TimerCallbacks,
portable = FALSE,
class = FALSE,
public = list(
# Empty constructor defaults to the getNow implementation
initialize = function() {
super$initialize(self$mockNow)
},
mockNow = function() {
return(private$time)
},
elapse = function(millis) {
private$time <- private$time + millis
},
getElapsed = function() {
private$time
}
), private = list(
time = 0L
)
)
timerCallbacks <- TimerCallbacks$new()
scheduleTask <- function(millis, callback) {
@@ -96,3 +121,27 @@ scheduleTask <- function(millis, callback) {
invisible(timerCallbacks$unschedule(id))
}
}
#' Get a scheduler function for scheduling tasks. Give priority to the
#' session scheduler, but if it doesn't exist, use the global one.
#' @noRd
defineScheduler <- function(session){
if (!is.null(session) && !is.null(session$.scheduleTask)){
return(session$.scheduleTask)
}
scheduleTask
}
#' Get the current time using the current reactive domain. This will try to use
#' the session's .now() method, but if that's not available, it will just return
#' the real time (from getTimeMs()). The purpose of this function is to allow
#' MockableTimerCallbacks to work.
#' @noRd
getDomainTimeMs <- function(session){
if (!is.null(session) && !is.null(session$.now)){
return(session$.now())
} else {
getTimeMs()
}
}

View File

@@ -1,8 +1,7 @@
#' Change the value of a text input on the client
#'
#' @template update-input
#' @param value The value to set for the input object.
#' @param placeholder The placeholder to set for the input object.
#' @inheritParams textInput
#'
#' @seealso [textInput()]
#'
@@ -35,7 +34,9 @@
#' shinyApp(ui, server)
#' }
#' @export
updateTextInput <- function(session, inputId, label = NULL, value = NULL, placeholder = NULL) {
updateTextInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL, placeholder = NULL) {
validate_session_object(session)
message <- dropNulls(list(label=label, value=value, placeholder=placeholder))
session$sendInputMessage(inputId, message)
}
@@ -82,7 +83,7 @@ updateTextAreaInput <- updateTextInput
#' Change the value of a checkbox input on the client
#'
#' @template update-input
#' @param value The value to set for the input object.
#' @inheritParams checkboxInput
#'
#' @seealso [checkboxInput()]
#'
@@ -107,7 +108,9 @@ updateTextAreaInput <- updateTextInput
#' shinyApp(ui, server)
#' }
#' @export
updateCheckboxInput <- function(session, inputId, label = NULL, value = NULL) {
updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL) {
validate_session_object(session)
message <- dropNulls(list(label=label, value=value))
session$sendInputMessage(inputId, message)
}
@@ -116,8 +119,7 @@ updateCheckboxInput <- function(session, inputId, label = NULL, value = NULL) {
#' Change the label or icon of an action button on the client
#'
#' @template update-input
#' @param icon The icon to set for the input object. To remove the
#' current icon, use `icon=character(0)`.
#' @inheritParams actionButton
#'
#' @seealso [actionButton()]
#'
@@ -126,13 +128,15 @@ updateCheckboxInput <- function(session, inputId, label = NULL, value = NULL) {
#' if (interactive()) {
#'
#' ui <- fluidPage(
#' actionButton("update", "Update other buttons"),
#' actionButton("update", "Update other buttons and link"),
#' br(),
#' actionButton("goButton", "Go"),
#' br(),
#' actionButton("goButton2", "Go 2", icon = icon("area-chart")),
#' br(),
#' actionButton("goButton3", "Go 3")
#' actionButton("goButton3", "Go 3"),
#' br(),
#' actionLink("goLink", "Go Link")
#' )
#'
#' server <- function(input, output, session) {
@@ -153,28 +157,34 @@ updateCheckboxInput <- function(session, inputId, label = NULL, value = NULL) {
#' # unchaged and changes its label
#' updateActionButton(session, "goButton3",
#' label = "New label 3")
#'
#' # Updates goLink's label and icon
#' updateActionButton(session, "goLink",
#' label = "New link label",
#' icon = icon("link"))
#' })
#' }
#'
#' shinyApp(ui, server)
#' }
#' @rdname updateActionButton
#' @export
updateActionButton <- function(session, inputId, label = NULL, icon = NULL) {
updateActionButton <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL) {
validate_session_object(session)
if (!is.null(icon)) icon <- as.character(validateIcon(icon))
message <- dropNulls(list(label=label, icon=icon))
session$sendInputMessage(inputId, message)
}
#' @rdname updateActionButton
#' @export
updateActionLink <- updateActionButton
#' Change the value of a date input on the client
#'
#' @template update-input
#' @param value The desired date value. Either a Date object, or a string in
#' `yyyy-mm-dd` format. Supply `NA` to clear the date.
#' @param min The minimum allowed date. Either a Date object, or a string in
#' `yyyy-mm-dd` format.
#' @param max The maximum allowed date. Either a Date object, or a string in
#' `yyyy-mm-dd` format.
#' @inheritParams dateInput
#'
#' @seealso [dateInput()]
#'
@@ -202,8 +212,10 @@ updateActionButton <- function(session, inputId, label = NULL, icon = NULL) {
#' shinyApp(ui, server)
#' }
#' @export
updateDateInput <- function(session, inputId, label = NULL, value = NULL,
min = NULL, max = NULL) {
updateDateInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL,
min = NULL, max = NULL)
{
validate_session_object(session)
value <- dateYMD(value, "value")
min <- dateYMD(min, "min")
@@ -217,14 +229,7 @@ updateDateInput <- function(session, inputId, label = NULL, value = NULL,
#' Change the start and end values of a date range input on the client
#'
#' @template update-input
#' @param start The start date. Either a Date object, or a string in
#' `yyyy-mm-dd` format. Supplying `NA` clears the start date.
#' @param end The end date. Either a Date object, or a string in
#' `yyyy-mm-dd` format. Supplying `NA` clears the end date.
#' @param min The minimum allowed date. Either a Date object, or a string in
#' `yyyy-mm-dd` format.
#' @param max The maximum allowed date. Either a Date object, or a string in
#' `yyyy-mm-dd` format.
#' @inheritParams dateRangeInput
#'
#' @seealso [dateRangeInput()]
#'
@@ -254,9 +259,11 @@ updateDateInput <- function(session, inputId, label = NULL, value = NULL,
#' shinyApp(ui, server)
#' }
#' @export
updateDateRangeInput <- function(session, inputId, label = NULL,
updateDateRangeInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL,
start = NULL, end = NULL, min = NULL,
max = NULL) {
max = NULL)
{
validate_session_object(session)
start <- dateYMD(start, "start")
end <- dateYMD(end, "end")
@@ -276,10 +283,10 @@ updateDateRangeInput <- function(session, inputId, label = NULL,
#' Change the selected tab on the client
#'
#' @param session The `session` object passed to function given to
#' `shinyServer`.
#' `shinyServer`. Default is `getDefaultReactiveDomain()`.
#' @param inputId The id of the `tabsetPanel`, `navlistPanel`,
#' or `navbarPage` object.
#' @param selected The name of the tab to make active.
#' @inheritParams tabsetPanel
#'
#' @seealso [tabsetPanel()], [navlistPanel()],
#' [navbarPage()]
@@ -312,7 +319,9 @@ updateDateRangeInput <- function(session, inputId, label = NULL,
#' shinyApp(ui, server)
#' }
#' @export
updateTabsetPanel <- function(session, inputId, selected = NULL) {
updateTabsetPanel <- function(session = getDefaultReactiveDomain(), inputId, selected = NULL) {
validate_session_object(session)
message <- dropNulls(list(value = selected))
session$sendInputMessage(inputId, message)
}
@@ -328,10 +337,7 @@ updateNavlistPanel <- updateTabsetPanel
#' Change the value of a number input on the client
#'
#' @template update-input
#' @param value The value to set for the input object.
#' @param min Minimum value.
#' @param max Maximum value.
#' @param step Step size.
#' @inheritParams numericInput
#'
#' @seealso [numericInput()]
#'
@@ -363,9 +369,11 @@ updateNavlistPanel <- updateTabsetPanel
#' shinyApp(ui, server)
#' }
#' @export
updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
updateNumericInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL,
min = NULL, max = NULL, step = NULL) {
validate_session_object(session)
message <- dropNulls(list(
label = label, value = formatNoSci(value),
min = formatNoSci(min), max = formatNoSci(max), step = formatNoSci(step)
@@ -378,12 +386,7 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
#' Change the value of a slider input on the client.
#'
#' @template update-input
#' @param value The value to set for the input object.
#' @param min Minimum value.
#' @param max Maximum value.
#' @param step Step size.
#' @param timeFormat Date and POSIXt formatting.
#' @param timezone The timezone offset for POSIXt objects.
#' @inheritParams sliderInput
#'
#' @seealso [sliderInput()]
#'
@@ -415,9 +418,11 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
#' )
#' }
#' @export
updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
updateSliderInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL,
min = NULL, max = NULL, step = NULL, timeFormat = NULL, timezone = NULL)
{
validate_session_object(session)
# If no min/max/value is provided, we won't know the
# type, and this will return an empty string
dataType <- getSliderType(min, max, value)
@@ -450,6 +455,8 @@ updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
selected = NULL, inline = FALSE, type = NULL,
choiceNames = NULL, choiceValues = NULL) {
validate_session_object(session)
if (is.null(type)) stop("Please specify the type ('checkbox' or 'radio')")
args <- normalizeChoicesArgs(choices, choiceNames, choiceValues, mustExist = FALSE)
@@ -507,9 +514,12 @@ updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
#' shinyApp(ui, server)
#' }
#' @export
updateCheckboxGroupInput <- function(session, inputId, label = NULL,
updateCheckboxGroupInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL,
choices = NULL, selected = NULL, inline = FALSE,
choiceNames = NULL, choiceValues = NULL) {
choiceNames = NULL, choiceValues = NULL)
{
validate_session_object(session)
updateInputOptions(session, inputId, label, choices, selected,
inline, "checkbox", choiceNames, choiceValues)
}
@@ -550,9 +560,12 @@ updateCheckboxGroupInput <- function(session, inputId, label = NULL,
#' shinyApp(ui, server)
#' }
#' @export
updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
updateRadioButtons <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, choices = NULL,
selected = NULL, inline = FALSE,
choiceNames = NULL, choiceValues = NULL) {
choiceNames = NULL, choiceValues = NULL)
{
validate_session_object(session)
# you must select at least one radio button
if (is.null(selected)) {
if (!is.null(choices)) selected <- choices[[1]]
@@ -602,11 +615,14 @@ updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
#' shinyApp(ui, server)
#' }
#' @export
updateSelectInput <- function(session, inputId, label = NULL, choices = NULL,
selected = NULL) {
updateSelectInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, choices = NULL,
selected = NULL)
{
validate_session_object(session)
choices <- if (!is.null(choices)) choicesWithNames(choices)
if (!is.null(selected)) selected <- as.character(selected)
options <- if (!is.null(choices)) selectOptions(choices, selected)
options <- if (!is.null(choices)) selectOptions(choices, selected, inputId, FALSE)
message <- dropNulls(list(label = label, options = options, value = selected))
session$sendInputMessage(inputId, message)
}
@@ -618,9 +634,12 @@ updateSelectInput <- function(session, inputId, label = NULL, choices = NULL,
#' `choices` into the page at once (i.e., only use the client-side
#' version of \pkg{selectize.js})
#' @export
updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
updateSelectizeInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, choices = NULL,
selected = NULL, options = list(),
server = FALSE) {
server = FALSE)
{
validate_session_object(session)
if (length(options)) {
res <- checkAsIs(options)
cfg <- tags$script(
@@ -733,12 +752,15 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
#' @rdname updateSelectInput
#' @inheritParams varSelectInput
#' @export
updateVarSelectInput <- function(session, inputId, label = NULL, data = NULL, selected = NULL) {
updateVarSelectInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, data = NULL, selected = NULL) {
validate_session_object(session)
if (is.null(data)) {
choices <- NULL
} else {
choices <- colnames(data)
}
updateSelectInput(
session = session,
inputId = inputId,
@@ -749,7 +771,11 @@ updateVarSelectInput <- function(session, inputId, label = NULL, data = NULL, se
}
#' @rdname updateSelectInput
#' @export
updateVarSelectizeInput <- function(session, inputId, label = NULL, data = NULL, selected = NULL, options = list(), server = FALSE) {
updateVarSelectizeInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL,
data = NULL, selected = NULL, options = list(), server = FALSE)
{
validate_session_object(session)
if (is.null(data)) {
choices <- NULL
} else {

110
R/utils-lang.R Normal file
View File

@@ -0,0 +1,110 @@
# Given a list of quosures, return a function that will evaluate them and return
# a list of resulting values. If the list contains a single expression, unwrap
# it from the list.
quos_to_func <- function(qs) {
if (length(qs) == 0) {
stop("Need at least one item in `...` to use as cache key or event.")
}
if (length(qs) == 1) {
# Special case for one quosure. This is needed for async to work -- that is,
# when the quosure returns a promise. It needs to not be wrapped into a list
# for the hybrid_chain stuff to detect that it's a promise. (Plus, it's not
# even clear what it would mean to mix promises and non-promises in the
# key.)
qs <- qs[[1]]
function() {
eval_tidy(qs)
}
} else {
function() {
lapply(qs, eval_tidy)
}
}
}
# Given a list of quosures, return a string representation of the expressions.
#
# qs <- list(quo(a+1), quo({ b+2; b + 3 }))
# quos_to_label(qs)
# #> [1] "a + 1, {\n b + 2\n b + 3\n}"
quos_to_label <- function(qs) {
res <- lapply(qs, function(q) {
paste(deparse(get_expr(q)), collapse = "\n")
})
paste(res, collapse = ", ")
}
# Get the formals and body for a function, without source refs. This is used for
# consistent hashing of the function.
formalsAndBody <- function(x) {
if (is.null(x)) {
return(list())
}
list(
formals = formals(x),
body = body(zap_srcref(x))
)
}
# This function is to be called from functions like `reactive()`, `observe()`,
# and the various render functions. It handles the following cases:
# - The typical case where x is an unquoted expression, and `env` and `quoted`
# are not used.
# - New-style metaprogramming cases, where rlang::inject() is used to inline a
# quosure into the AST, as in `inject(reactive(!!x))`.
# - Old-style metaprogramming cases, where `env` and/or `quoted` are used.
#
# Much of the complexity is handling old-style metaprogramming cases. The code
# in this function is more complicated because it needs to look at unevaluated
# expressions in the _calling_ function. If this code were put directly in the
# calling function, it would look like this:
#
# if (!missing(env) || !missing(quoted)) {
# deprecatedEnvQuotedMessage()
# if (!quoted) x <- substitute(x)
# x <- new_quosure(x, env)
#
# } else {
# x <- substitute(x)
# if (!is_quosure(x)) {
# x <- new_quosure(x, env = parent.frame())
# }
# }
#
# In the future, the calling functions will not need to have the `env` and
# `quoted` arguments -- `rlang::inject()` and quosures can be used instead.
# Instead of using this function, `get_quosure()`, the caller can instead use
# just the following code:
#
# x <- substitute(x)
# if (!is_quosure(x)) {
# x <- new_quosure(x, env = parent.frame())
# }
#
get_quosure <- function(x, env, quoted) {
if (!eval(substitute(missing(env)), parent.frame()) ||
!eval(substitute(missing(quoted)), parent.frame()))
{
deprecatedEnvQuotedMessage()
if (!quoted) {
x <- eval(substitute(substitute(x)), parent.frame())
}
x <- new_quosure(x, env)
} else {
x <- eval(substitute(substitute(x)), parent.frame())
# At this point, x can be a quosure if rlang::inject() is used, but the
# typical case is that x is not a quosure.
if (!is_quosure(x)) {
x <- new_quosure(x, env = parent.frame(2L))
}
}
x
}

448
R/utils.R
View File

@@ -113,24 +113,6 @@ isWholeNum <- function(x, tol = .Machine$double.eps^0.5) {
abs(x - round(x)) < tol
}
`%OR%` <- function(x, y) {
if (is.null(x) || isTRUE(is.na(x)))
y
else
x
}
`%AND%` <- function(x, y) {
if (!is.null(x) && !isTRUE(is.na(x)))
if (!is.null(y) && !isTRUE(is.na(y)))
return(y)
return(NULL)
}
`%.%` <- function(x, y) {
paste(x, y, sep='')
}
# Given a vector or list, drop all the NULL items in it
dropNulls <- function(x) {
x[!vapply(x, is.null, FUN.VALUE=logical(1))]
@@ -182,6 +164,10 @@ asNamed <- function(x) {
x
}
empty_named_list <- function() {
list(a = 1)[0]
}
# Given two named vectors, join them together, and keep only the last element
# with a given name in the resulting vector. If b has any elements with the same
# name as elements in a, the element in a is dropped. Also, if there are any
@@ -210,6 +196,17 @@ sortByName <- function(x) {
x[order(names(x))]
}
# Sort a vector. If a character vector, sort using C locale, which is consistent
# across platforms. Note that radix sort uses C locale according to ?sort.
sort_c <- function(x, ...) {
# Use UTF-8 encoding, because if encoding is "unknown" for non-ASCII
# characters, the sort() will throw an error.
if (is.character(x))
x <- enc2utf8(x)
sort(x, method = "radix", ...)
}
# Wrapper around list2env with a NULL check. In R <3.2.0, if an empty unnamed
# list is passed to list2env(), it errors. But an empty named list is OK. For
# R >=3.2.0, this wrapper is not necessary.
@@ -316,6 +313,15 @@ resolve <- function(dir, relpath) {
return(abs.path)
}
# Given a string, make sure it has a trailing slash.
ensure_trailing_slash <- function(path) {
if (!grepl("/$", path)) {
path <- paste0(path, "/")
}
path
}
isWindows <- function() .Platform$OS.type == 'windows'
# This is a wrapper for download.file and has the same interface.
@@ -408,7 +414,8 @@ makeFunction <- function(args = pairlist(), body, env = parent.frame()) {
#' Convert an expression to a function
#'
#' This is to be called from another function, because it will attempt to get
#' an unquoted expression from two calls back.
#' an unquoted expression from two calls back. Note: as of Shiny 1.6.0, it is
#' recommended to use [quoToFunction()] instead.
#'
#' If expr is a quoted expression, then this just converts it to a function.
#' If expr is a function, then this simply returns expr (and prints a
@@ -466,7 +473,8 @@ exprToFunction <- function(expr, env=parent.frame(), quoted=FALSE) {
#' Install an expression as a function
#'
#' Installs an expression in the given environment as a function, and registers
#' debug hooks so that breakpoints may be set in the function.
#' debug hooks so that breakpoints may be set in the function. Note: as of
#' Shiny 1.6.0, it is recommended to use [quoToFunction()] instead.
#'
#' This function can replace `exprToFunction` as follows: we may use
#' `func <- exprToFunction(expr)` if we do not want the debug hooks, or
@@ -512,6 +520,48 @@ installExprFunction <- function(expr, name, eval.env = parent.frame(2),
assign(name, func, envir = assign.env)
}
#' Convert a quosure to a function for a Shiny render function
#'
#' This takes a quosure and label, and wraps them into a function that should be
#' passed to [createRenderFunction()] or [markRenderFunction()].
#'
#' This function was added in Shiny 1.6.0. Previously, it was recommended to use
#' [installExprFunction()] or [exprToFunction()] in render functions, but now we
#' recommend using [quoToFunction()], because it does not require `env` and
#' `quoted` arguments -- that information is captured by quosures provided by
#' \pkg{rlang}.
#'
#' @param q A quosure.
#' @inheritParams installExprFunction
#' @seealso [createRenderFunction()] for example usage.
#'
#' @export
quoToFunction <- function(q, label, ..stacktraceon = FALSE) {
q <- as_quosure(q)
# Use new_function() instead of as_function(), because as_function() adds an
# extra parent environment. (This may not actually be a problem, though.)
func <- new_function(NULL, get_expr(q), get_env(q))
wrapFunctionLabel(func, label, ..stacktraceon = ..stacktraceon)
}
# Utility function for creating a debugging label, given an expression.
# `expr` is a quoted expression.
# `function_name` is the name of the calling function.
# `label` is an optional user-provided label. If NULL, it will be inferred.
exprToLabel <- function(expr, function_name, label = NULL) {
srcref <- attr(expr, "srcref", exact = TRUE)
if (is.null(label)) {
label <- rexprSrcrefToLabel(
srcref[[1]],
sprintf('%s(%s)', function_name, paste(deparse(expr), collapse = '\n'))
)
}
if (length(srcref) >= 2) attr(label, "srcref") <- srcref[[2]]
attr(label, "srcfile") <- srcFileOfRef(srcref[[1]])
label
}
#' Parse a GET query string from a URL
#'
#' Returns a named list of key-value pairs.
@@ -612,37 +662,6 @@ shinyCallingHandlers <- function(expr) {
)
}
#' Print message for deprecated functions in Shiny
#'
#' To disable these messages, use `options(shiny.deprecation.messages=FALSE)`.
#'
#' @param new Name of replacement function.
#' @param msg Message to print. If used, this will override the default message.
#' @param old Name of deprecated function.
#' @param version The last version of Shiny before the item was deprecated.
#' @keywords internal
shinyDeprecated <- function(new=NULL, msg=NULL,
old=as.character(sys.call(sys.parent()))[1L],
version = NULL) {
if (getOption("shiny.deprecation.messages") %OR% TRUE == FALSE)
return(invisible())
if (is.null(msg)) {
msg <- paste(old, "is deprecated.")
if (!is.null(new)) {
msg <- paste(msg, "Please use", new, "instead.",
"To disable this message, run options(shiny.deprecation.messages=FALSE)")
}
}
if (!is.null(version)) {
msg <- paste0(msg, " (Last used in version ", version, ")")
}
# Similar to .Deprecated(), but print a message instead of warning
message(msg)
}
#' Register a function with the debugger (if one is active).
#'
@@ -800,7 +819,14 @@ dataTablesJSON <- function(data, req) {
fdata <- unname(as.matrix(fdata))
if (is.character(fdata) && q$escape != 'false') {
if (q$escape == 'true') fdata <- htmlEscape(fdata) else {
if (q$escape == 'true') {
# fdata must be a matrix at this point, and we need to preserve
# dimensions. Note that it could be a 1xn matrix.
dims <- dim(fdata)
fdata <- htmlEscape(fdata)
dim(fdata) <- dims
} else {
k <- as.integer(strsplit(q$escape, ',')[[1]])
# use seq_len() in case escape = negative indices, e.g. c(-1, -5)
for (j in seq_len(ncol(fdata))[k]) fdata[, j] <- htmlEscape(fdata[, j])
@@ -1085,52 +1111,39 @@ reactiveStop <- function(message = "", class = NULL) {
#' Validate input values and other conditions
#'
#' For an output rendering function (e.g. [renderPlot()]), you may
#' need to check that certain input values are available and valid before you
#' can render the output. `validate` gives you a convenient mechanism for
#' doing so.
#' @description
#' `validate()` provides convenient mechanism for validating that an output
#' has all the inputs necessary for successful rendering. It takes any number
#' of (unnamed) arguments, each representing a condition to test. If any
#' of condition fails (i.e. is not ["truthy"][isTruthy]), a special type of
#' error is signaled to stop execution. If this error is not handled by
#' application-specific code, it is displayed to the user by Shiny.
#'
#' The `validate` function takes any number of (unnamed) arguments, each of
#' which represents a condition to test. If any of the conditions represent
#' failure, then a special type of error is signaled which stops execution. If
#' this error is not handled by application-specific code, it is displayed to
#' the user by Shiny.
#' If you use `validate()` in a [reactive()] validation failures will
#' automatically propagate to outputs that use the reactive.
#'
#' An easy way to provide arguments to `validate` is to use the `need`
#' function, which takes an expression and a string; if the expression is
#' considered a failure, then the string will be used as the error message. The
#' `need` function considers its expression to be a failure if it is any of
#' the following:
#' @section `need()`:
#' An easy way to provide arguments to `validate()` is to use `need()`, which
#' takes an expression and a string. If the expression is not
#' ["truthy"][isTruthy] then the string will be used as the error message.
#'
#' \itemize{
#' \item{`FALSE`}
#' \item{`NULL`}
#' \item{`""`}
#' \item{An empty atomic vector}
#' \item{An atomic vector that contains only missing values}
#' \item{A logical vector that contains all `FALSE` or missing values}
#' \item{An object of class `"try-error"`}
#' \item{A value that represents an unclicked [actionButton()]}
#' If "truthiness" is flexible for your use case, you'll need to explicitly
#' generate a logical values. For example, if you want allow `NA` but not
#' `NULL`, you can `!is.null(input$foo)`.
#'
#' If you need validation logic that differs significantly from `need()`, you
#' can create your own validation test functions. A passing test should return
#' `NULL`. A failing test should return either a string providing the error
#' to display to the user, or if the failure should happen silently, `FALSE`.
#'
#' Alternatively you can use `validate()` within an `if` statement, which is
#' particularly useful for more complex conditions:
#'
#' ```
#' if (input$x < 0 && input$choice == "positive") {
#' validate("If choice is positive then x must be greater than 0")
#' }
#'
#' If any of these values happen to be valid, you can explicitly turn them to
#' logical values. For example, if you allow `NA` but not `NULL`, you
#' can use the condition `!is.null(input$foo)`, because `!is.null(NA)
#' == TRUE`.
#'
#' If you need validation logic that differs significantly from `need`, you
#' can create other validation test functions. A passing test should return
#' `NULL`. A failing test should return an error message as a
#' single-element character vector, or if the failure should happen silently,
#' `FALSE`.
#'
#' Because validation failure is signaled as an error, you can use
#' `validate` in reactive expressions, and validation failures will
#' automatically propagate to outputs that use the reactive expression. In
#' other words, if reactive expression `a` needs `input$x`, and two
#' outputs use `a` (and thus depend indirectly on `input$x`), it's
#' not necessary for the outputs to validate `input$x` explicitly, as long
#' as `a` does validate it.
#' ```
#'
#' @param ... A list of tests. Each test should equal `NULL` for success,
#' `FALSE` for silent failure, or a string for failure with an error
@@ -1207,7 +1220,7 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
#' Check for required values
#'
#' Ensure that values are available ("truthy"--see Details) before proceeding
#' Ensure that values are available (["truthy"][isTruthy]) before proceeding
#' with a calculation or action. If any of the given values is not truthy, the
#' operation is stopped by raising a "silent" exception (not logged by Shiny,
#' nor displayed in the Shiny app's UI).
@@ -1216,11 +1229,13 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
#' is to call it like a statement (ignoring its return value) before attempting
#' operations using the required values:
#'
#' \preformatted{rv <- reactiveValues(state = FALSE)
#' ```
#' rv <- reactiveValues(state = FALSE)
#' r <- reactive({
#' req(input$a, input$b, rv$state)
#' # Code that uses input$a, input$b, and/or rv$state...
#' })}
#' })
#' ```
#'
#' In this example, if `r()` is called and any of `input$a`,
#' `input$b`, and `rv$state` are `NULL`, `FALSE`, `""`,
@@ -1229,54 +1244,21 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
#'
#' The second is to use it to wrap an expression that must be truthy:
#'
#' \preformatted{output$plot <- renderPlot({
#' ```
#' output$plot <- renderPlot({
#' if (req(input$plotType) == "histogram") {
#' hist(dataset())
#' } else if (input$plotType == "scatter") {
#' qplot(dataset(), aes(x = x, y = y))
#' }
#' })}
#' })
#' ```
#'
#' In this example, `req(input$plotType)` first checks that
#' `input$plotType` is truthy, and if so, returns it. This is a convenient
#' way to check for a value "inline" with its first use.
#'
#' **Truthy and falsy values**
#'
#' The terms "truthy" and "falsy" generally indicate whether a value, when
#' coerced to a [base::logical()], is `TRUE` or `FALSE`. We use
#' the term a little loosely here; our usage tries to match the intuitive
#' notions of "Is this value missing or available?", or "Has the user provided
#' an answer?", or in the case of action buttons, "Has the button been
#' clicked?".
#'
#' For example, a `textInput` that has not been filled out by the user has
#' a value of `""`, so that is considered a falsy value.
#'
#' To be precise, `req` considers a value truthy *unless* it is one
#' of:
#'
#' \itemize{
#' \item{`FALSE`}
#' \item{`NULL`}
#' \item{`""`}
#' \item{An empty atomic vector}
#' \item{An atomic vector that contains only missing values}
#' \item{A logical vector that contains all `FALSE` or missing values}
#' \item{An object of class `"try-error"`}
#' \item{A value that represents an unclicked [actionButton()]}
#' }
#'
#' Note in particular that the value `0` is considered truthy, even though
#' `as.logical(0)` is `FALSE`.
#'
#' If the built-in rules for truthiness do not match your requirements, you can
#' always work around them. Since `FALSE` is falsy, you can simply provide
#' the results of your own checks to `req`:
#'
#' `req(input$a != 0)`
#'
#' **Using `req(FALSE)`**
#' @section Using `req(FALSE)`:
#'
#' You can use `req(FALSE)` (i.e. no condition) if you've already performed
#' all the checks you needed to by that point and just want to stop the reactive
@@ -1284,7 +1266,7 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
#' if you have a complicated condition to check for (or perhaps if you'd like to
#' divide your condition into nested `if` statements).
#'
#' **Using `cancelOutput = TRUE`**
#' @section Using `cancelOutput = TRUE`:
#'
#' When `req(..., cancelOutput = TRUE)` is used, the "silent" exception is
#' also raised, but it is treated slightly differently if one or more outputs are
@@ -1303,7 +1285,6 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
#' @param cancelOutput If `TRUE` and an output is being evaluated, stop
#' processing as usual but instead of clearing the output, leave it in
#' whatever state it happens to be in.
#' @param x An expression whose truthiness value we want to determine
#' @return The first value that was passed in.
#' @export
#' @examples
@@ -1393,14 +1374,40 @@ cancelOutput <- function() {
#
# Can be used to facilitate short-circuit eval on dots.
dotloop <- function(fun_, ...) {
for (i in 1:(nargs()-1)) {
for (i in seq_len(nargs() - 1)) {
fun_(eval(as.symbol(paste0("..", i))))
}
invisible()
}
#' Truthy and falsy values
#'
#' The terms "truthy" and "falsy" generally indicate whether a value, when
#' coerced to a [base::logical()], is `TRUE` or `FALSE`. We use
#' the term a little loosely here; our usage tries to match the intuitive
#' notions of "Is this value missing or available?", or "Has the user provided
#' an answer?", or in the case of action buttons, "Has the button been
#' clicked?".
#'
#' For example, a `textInput` that has not been filled out by the user has
#' a value of `""`, so that is considered a falsy value.
#'
#' To be precise, a value is truthy *unless* it is one of:
#'
#' * `FALSE`
#' * `NULL`
#' * `""`
#' * An empty atomic vector
#' * An atomic vector that contains only missing values
#' * A logical vector that contains all `FALSE` or missing values
#' * An object of class `"try-error"`
#' * A value that represents an unclicked [actionButton()]
#'
#' Note in particular that the value `0` is considered truthy, even though
#' `as.logical(0)` is `FALSE`.
#'
#' @param x An expression whose truthiness value we want to determine
#' @export
#' @rdname req
isTruthy <- function(x) {
if (inherits(x, 'try-error'))
return(FALSE)
@@ -1477,7 +1484,7 @@ checkEncoding <- function(file) {
if (identical(charToRaw(readChar(file, 3L, TRUE)), charToRaw('\UFEFF'))) {
warning('You should not include the Byte Order Mark (BOM) in ', file, '. ',
'Please re-save it in UTF-8 without BOM. See ',
'http://shiny.rstudio.com/articles/unicode.html for more info.')
'https://shiny.rstudio.com/articles/unicode.html for more info.')
return('UTF-8-BOM')
}
x <- readChar(file, size, useBytes = TRUE)
@@ -1561,15 +1568,19 @@ URLencode <- function(value, reserved = FALSE) {
if (reserved) encodeURIComponent(value) else encodeURI(value)
}
# Make user-supplied dates are either NULL or can be coerced
# to a yyyy-mm-dd formatted string. If a date is specified, this
# function returns a string for consistency across locales.
# Also, `as.Date()` is used to coerce strings to date objects
# so that strings like "2016-08-9" are expanded to "2016-08-09"
# Make sure user-supplied dates are either NULL or can be coerced to a
# yyyy-mm-dd formatted string. If a date is specified, this function returns a
# string for consistency across locales. Also, `as.Date()` is used to coerce
# strings to date objects so that strings like "2016-08-9" are expanded to
# "2016-08-09". If any of the values result in error or NA, then the input
# `date` is returned unchanged.
dateYMD <- function(date = NULL, argName = "value") {
if (!length(date)) return(NULL)
if (length(date) > 1) warning("Expected `", argName, "` to be of length 1.")
tryCatch(date <- format(as.Date(date), "%Y-%m-%d"),
tryCatch({
res <- format(as.Date(date), "%Y-%m-%d")
if (any(is.na(res))) stop()
date <- res
},
error = function(e) {
warning(
"Couldn't coerce the `", argName,
@@ -1592,18 +1603,17 @@ wrapFunctionLabel <- function(func, name, ..stacktraceon = FALSE) {
assign(name, func, environment())
registerDebugHook(name, environment(), name)
relabelWrapper <- eval(substitute(
function(...) {
# This `f` gets renamed to the value of `name`. Note that it may not
# print as the new name, because of source refs stored in the function.
if (..stacktraceon)
..stacktraceon..(f(...))
else
f(...)
},
list(f = as.name(name))
))
if (..stacktraceon) {
# We need to wrap the `...` in `!!quote(...)` so that R CMD check won't
# complain about "... may be used in an incorrect context"
body <- expr({ ..stacktraceon..((!!name)(!!quote(...))) })
} else {
body <- expr({ (!!name)(!!quote(...)) })
}
relabelWrapper <- new_function(pairlist2(... =), body, environment())
# Preserve the original function that was passed in; is used for caching.
attr(relabelWrapper, "wrappedFunc") <- func
relabelWrapper
}
@@ -1663,19 +1673,23 @@ hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
if (promises::is.promising(result$value)) {
# Purposefully NOT including domain (nor replace), as we're already in
# the domain at this point
p <- promise_chain(setVisible(result), ..., catch = catch, finally = finally)
p <- promise_chain(valueWithVisible(result), ..., catch = catch, finally = finally)
runFinally <- FALSE
p
} else {
result <- Reduce(function(v, func) {
if (".visible" %in% names(formals(func))) {
withVisible(func(v$value, .visible = v$visible))
} else {
withVisible(func(v$value))
}
}, list(...), result)
result <- Reduce(
function(v, func) {
if (v$visible) {
withVisible(func(v$value))
} else {
withVisible(func(invisible(v$value)))
}
},
list(...),
result
)
setVisible(result)
valueWithVisible(result)
}
})
},
@@ -1696,24 +1710,13 @@ hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
}
}
# Returns `value` with either `invisible()` applied or not, depending on the
# value of `visible`.
#
# If the `visible` is missing, then `value` should be a list as returned from
# `withVisible()`, and that visibility will be applied.
setVisible <- function(value, visible) {
if (missing(visible)) {
visible <- value$visible
value <- value$value
}
if (!visible) {
invisible(value)
} else {
(value)
}
# Given a list with items named `value` and `visible`, return `x$value` either
# visibly, or invisibly, depending on the value of `x$visible`.
valueWithVisible <- function(x) {
if (x$visible) x$value else invisible(x$value)
}
createVarPromiseDomain <- function(env, name, value) {
force(env)
force(name)
@@ -1757,7 +1760,10 @@ getSliderType <- function(min, max, value) {
else "number"
}))
if (length(type) > 1) {
stop("Type mismatch for `min`, `max`, and `value`. Each must be Date, POSIXt, or number.")
rlang::abort(c(
"Type mismatch for `min`, `max`, and `value`.",
"All values must either be numeric, Date, or POSIXt."
))
}
type[[1]]
}
@@ -1805,3 +1811,77 @@ cat_line <- function(...) {
cat(paste(..., "\n", collapse = ""))
}
select_menu <- function(choices, title = NULL, msg = "Enter one or more numbers (with spaces), or an empty line to exit: \n")
{
if (!is.null(title)) {
cat(title, "\n", sep = "")
}
nc <- length(choices)
op <- paste0(format(seq_len(nc)), ": ", choices)
fop <- format(op)
cat("", fop, "", sep = "\n")
repeat {
answer <- readline(msg)
answer <- strsplit(answer, "[ ,]+")[[1]]
if (all(answer %in% seq_along(choices))) {
return(choices[as.integer(answer)])
}
}
}
#' @noRd
isAppDir <- function(path) {
if (file.exists(file.path.ci(path, "app.R")))
return(TRUE)
if (file.exists(file.path.ci(path, "server.R"))
&& file.exists(file.path.ci(path, "ui.R")))
return(TRUE)
FALSE
}
# Borrowed from rprojroot which borrowed from devtools
#' @noRd
is_root <- function(path) {
identical(
normalizePath(path, winslash = "/"),
normalizePath(dirname(path), winslash = "/")
)
}
#' @noRd
findEnclosingApp <- function(path = ".") {
orig_path <- path
path <- normalizePath(path, winslash = "/", mustWork = TRUE)
repeat {
if (isAppDir(path))
return(path)
if (is_root(path))
stop("Shiny app not found at ", orig_path, " or in any parent directory.")
path <- dirname(path)
}
}
# Check if a package is installed, and if version is specified,
# that we have at least that version
is_available <- function(package, version = NULL) {
installed <- nzchar(system.file(package = package))
if (is.null(version)) {
return(installed)
}
installed && isTRUE(utils::packageVersion(package) >= version)
}
# cached version of utils::packageVersion("shiny")
shinyPackageVersion <- local({
version <- NULL
function() {
if (is.null(version)) {
version <<- utils::packageVersion("shiny")
}
version
}
})

2
R/version_jquery.R Normal file
View File

@@ -0,0 +1,2 @@
# Generated by tools/updatejQuery.R; do not edit by hand
version_jquery <- "3.6.0"

56
R/viewer.R Normal file
View File

@@ -0,0 +1,56 @@
#' 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)
}
}

View File

@@ -1,65 +1,57 @@
# Shiny
# shiny <img src="man/figures/logo.png" align="right" width=120 height=139 alt="" />
*Travis:* [![Travis Build Status](https://travis-ci.org/rstudio/shiny.svg?branch=master)](https://travis-ci.org/rstudio/shiny)
<!-- badges: start -->
[![CRAN](https://www.r-pkg.org/badges/version/shiny)](https://CRAN.R-project.org/package=shiny)
[![R build status](https://github.com/rstudio/shiny/workflows/R-CMD-check/badge.svg)](https://github.com/rstudio/shiny/actions)
[![RStudio community](https://img.shields.io/badge/community-shiny-blue?style=social&logo=rstudio&logoColor=75AADB)](https://community.rstudio.com/new-topic?category=shiny&tags=shiny)
*AppVeyor:* [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/rstudio/shiny?branch=master&svg=true)](https://ci.appveyor.com/project/rstudio/shiny)
<!-- badges: end -->
Shiny is a new package from RStudio that makes it incredibly easy to build interactive web applications with R.
For an introduction and examples, visit the [Shiny Dev Center](http://shiny.rstudio.com/).
If you have general questions about using Shiny, please use the [RStudio Community website](https://community.rstudio.com). For bug reports, please use the [issue tracker](https://github.com/rstudio/shiny/issues).
Easily build rich and productive interactive web apps in R &mdash; no HTML/CSS/JavaScript required.
## Features
* Build useful web applications with only a few lines of code&mdash;no JavaScript required.
* Shiny applications are automatically "live" in the same way that spreadsheets are live. Outputs change instantly as users modify inputs, without requiring a reload of the browser.
* Shiny user interfaces can be built entirely using R, or can be written directly in HTML, CSS, and JavaScript for more flexibility.
* Works in any R environment (Console R, Rgui for Windows or Mac, ESS, StatET, RStudio, etc.).
* Attractive default UI theme based on [Bootstrap](http://getbootstrap.com/).
* A highly customizable slider widget with built-in support for animation.
* Prebuilt output widgets for displaying plots, tables, and printed output of R objects.
* Fast bidirectional communication between the web browser and R using the [httpuv](https://github.com/rstudio/httpuv) package.
* Uses a [reactive](http://en.wikipedia.org/wiki/Reactive_programming) programming model that eliminates messy event handling code, so you can focus on the code that really matters.
* Develop and redistribute your own Shiny widgets that other developers can easily drop into their own applications (coming soon!).
* An intuitive and extensible [reactive programming](https://en.wikipedia.org/wiki/Reactive_programming) model which makes it easy to transform existing R code into a "live app" where outputs automatically react to new user input.
* Compared to event-based programming, reactivity allows Shiny to do the minimum amount of work when input(s) change, and allows humans to more easily reason about complex [MVC logic](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller).
* A prebuilt set of highly sophisticated, customizable, and easy-to-use widgets (e.g., plots, tables, sliders, dropdowns, date pickers, and more).
* An attractive default look based on [Bootstrap](https://getbootstrap.com/) which can also be easily customized with the [bslib](https://github.com/rstudio/bslib) package or avoided entirely with more direct R bindings to HTML/CSS/JavaScript.
* Seamless integration with [R Markdown](https://shiny.rstudio.com/articles/interactive-docs.html), making it easy to embed numerous applications natively within a larger dynamic document.
* Tools for improving and monitoring performance, including native support for [async programming](https://blog.rstudio.com/2018/06/26/shiny-1-1-0/), [caching](https://talks.cpsievert.me/20201117), [load testing](https://rstudio.github.io/shinyloadtest/), and [more](https://support.rstudio.com/hc/en-us/articles/231874748-Scaling-and-Performance-Tuning-in-RStudio-Connect).
* [Modules](https://shiny.rstudio.com/articles/modules.html): a framework for reducing code duplication and complexity.
* An ability to [bookmark application state](https://shiny.rstudio.com/articles/bookmarking-state.html) and/or [generate code to reproduce output(s)](https://github.com/rstudio/shinymeta).
* A rich ecosystem of extension packages for more [custom widgets](http://www.htmlwidgets.org/), [input validation](https://github.com/rstudio/shinyvalidate), [unit testing](https://github.com/rstudio/shinytest), and more.
## Installation
To install the stable version from CRAN, simply run the following from an R console:
To install the stable version from CRAN:
```r
install.packages("shiny")
```
To install the latest development builds directly from GitHub, run this instead:
```r
if (!require("devtools"))
install.packages("devtools")
devtools::install_github("rstudio/shiny")
```
## Getting Started
To learn more we highly recommend you check out the [Shiny Tutorial](http://shiny.rstudio.com/tutorial/). The tutorial explains the framework in-depth, walks you through building a simple application, and includes extensive annotated examples.
Once installed, load the library and run an example:
## Bootstrap 3 migration
Shiny versions 0.10.2.2 and below used the Bootstrap 2 web framework. After 0.10.2.2, Shiny switched to Bootstrap 3. For most users, the upgrade should be seamless. However, if you have have customized your HTML-generating code to use features specific to Bootstrap 2, you may need to update your code to work with Bootstrap 3.
If you do not wish to update your code at this time, you can use the [shinybootstrap2](https://github.com/rstudio/shinybootstrap2) package for backward compatibility.
If you prefer to install an older version of Shiny, you can do it using the devtools package:
```R
devtools::install_version("shiny", version = "0.10.2.2")
```r
library(shiny)
# Launches an app, with the app's source code included
runExample("06_tabsets")
# Lists more prepackaged examples
runExample()
```
## Development notes
For more examples and inspiration, check out the [Shiny User Gallery](https://shiny.rstudio.com/gallery/).
The Javascript code in Shiny is minified using tools that run on Node.js. See the tools/ directory for more information.
For help with learning fundamental Shiny programming concepts, check out the [Mastering Shiny](https://mastering-shiny.org/) book and the [Shiny Tutorial](https://shiny.rstudio.com/tutorial/). The former is currently more up-to-date with modern Shiny features, whereas the latter takes a deeper, more visual, dive into fundamental concepts.
## Guidelines for contributing
## Getting Help
To ask a question about Shiny, please use the [RStudio Community website](https://community.rstudio.com/new-topic?category=shiny&tags=shiny).
For bug reports, please use the [issue tracker](https://github.com/rstudio/shiny/issues) and also keep in mind that by [writing a good bug report](https://github.com/rstudio/shiny/wiki/Writing-Good-Bug-Reports), you're more likely to get help with your problem.
## Contributing
We welcome contributions to the **shiny** package. Please see our [CONTRIBUTING.md](https://github.com/rstudio/shiny/blob/master/.github/CONTRIBUTING.md) file for detailed guidelines of how to contribute.

View File

@@ -1,52 +0,0 @@
# DO NOT CHANGE the "init" and "install" sections below
# Download script file from GitHub
init:
ps: |
$ErrorActionPreference = "Stop"
Invoke-WebRequest http://raw.github.com/krlmlr/r-appveyor/master/scripts/appveyor-tool.ps1 -OutFile "..\appveyor-tool.ps1"
Import-Module '..\appveyor-tool.ps1'
install:
ps: Bootstrap
cache:
- C:\RLibrary
# Adapt as necessary starting from here
build_script:
- travis-tool.sh install_github rstudio/htmltools@rc-v0.4.0
- travis-tool.sh install_github rstudio/promises@rc-v1.1.0
- travis-tool.sh install_github r-lib/later@rc-v1.0.0
- travis-tool.sh install_deps
test_script:
- travis-tool.sh run_tests
on_failure:
- 7z a failure.zip *.Rcheck\*
- appveyor PushArtifact failure.zip
artifacts:
- path: '*.Rcheck\**\*.log'
name: Logs
- path: '*.Rcheck\**\*.out'
name: Logs
- path: '*.Rcheck\**\*.fail'
name: Logs
- path: '*.Rcheck\**\*.Rout'
name: Logs
- path: '\*_*.tar.gz'
name: Bits
- path: '\*_*.zip'
name: Bits
environment:
global:
USE_RTOOLS: true

View File

@@ -0,0 +1,27 @@
exampleModuleUI <- function(id, label = "Counter") {
# All uses of Shiny input/output IDs in the UI must be namespaced,
# as in ns("x").
ns <- NS(id)
tagList(
actionButton(ns("button"), label = label),
verbatimTextOutput(ns("out"))
)
}
exampleModuleServer <- function(id) {
# moduleServer() wraps a function to create the server component of a
# module.
moduleServer(
id,
function(input, output, session) {
count <- reactiveVal(0)
observeEvent(input$button, {
count(count() + 1)
})
output$out <- renderText({
count()
})
count
}
)
}

View File

@@ -0,0 +1,5 @@
# Given a numeric vector, convert to strings, sort, and convert back to
# numeric.
lexical_sort <- function(x) {
as.numeric(sort(as.character(x)))
}

56
inst/app_template/app.R Normal file
View File

@@ -0,0 +1,56 @@
ui <- fluidPage(
{{
# These blocks of code are processed with htmlTemplate()
if (isTRUE(module)) {
' # ======== Modules ========
# exampleModuleUI is defined in R/example-module.R
wellPanel(
h2("Modules example"),
exampleModuleUI("examplemodule1", "Click counter #1"),
exampleModuleUI("examplemodule2", "Click counter #2")
),
# =========================
'
}
}}
wellPanel(
h2("Sorting example"),
sliderInput("size", "Data size", min = 5, max = 20, value = 10),
{{
if (isTRUE(rdir)) {
' div("Lexically sorted sequence:"),'
} else {
' div("Sorted sequence:"),'
}
}}
verbatimTextOutput("sequence")
)
)
server <- function(input, output, session) {
{{
if (isTRUE(module)) {
' # ======== Modules ========
# exampleModuleServer is defined in R/example-module.R
exampleModuleServer("examplemodule1")
exampleModuleServer("examplemodule2")
# =========================
'
}
}}
data <- reactive({
{{
if (isTRUE(rdir)) {
' # lexical_sort from R/example.R
lexical_sort(seq_len(input$size))'
} else {
' sort(seq_len(input$size))'
}
}}
})
output$sequence <- renderText({
paste(data(), collapse = " ")
})
}
shinyApp(ui, server)

View File

@@ -0,0 +1,2 @@
library(shinytest)
expect_pass(testApp("../", suffix = osName()))

View File

@@ -0,0 +1,12 @@
app <- ShinyDriver$new("../../")
app$snapshotInit("mytest")
app$snapshot()
{{
if (isTRUE(module)) {
'
app$setInputs(`examplemodule1-button` = "click")
app$setInputs(`examplemodule1-button` = "click")
app$snapshot()'
}
}}

View File

@@ -0,0 +1,9 @@
library(testthat)
test_dir(
"./testthat",
# Run in the app's environment containing all support methods.
env = shiny::loadSupport(),
# Display the regular progress output and throw an error if any test error is found
reporter = c("progress", "fail")
)

View File

@@ -0,0 +1,16 @@
# See ?testServer for more information
testServer(exampleModuleServer, {
# Set initial value of a button
session$setInputs(button = 0)
# Check the value of the reactiveVal `count()`
expect_equal(count(), 1)
# Check the value of the renderText()
expect_equal(output$out, "1")
# Simulate a click
session$setInputs(button = 1)
expect_equal(count(), 2)
expect_equal(output$out, "2")
})

View File

@@ -0,0 +1,18 @@
testServer(expr = {
# Set the `size` slider and check the output
session$setInputs(size = 6)
expect_equal(output$sequence, "1 2 3 4 5 6")
{{
if (isTRUE(rdir)) {
'
session$setInputs(size = 12)
expect_equal(output$sequence, "1 10 11 12 2 3 4 5 6 7 8 9")
'
} else {
'
session$setInputs(size = 12)
expect_equal(output$sequence, "1 2 3 4 5 6 7 8 9 10 11 12")
'
}
}}
})

View File

@@ -0,0 +1,5 @@
# Test the lexical_sort function from R/example.R
test_that("Lexical sorting works", {
expect_equal(lexical_sort(c(1, 2, 3)), c(1, 2, 3))
expect_equal(lexical_sort(c(1, 2, 3, 13, 11, 21)), c(1, 11, 13, 2, 21, 3))
})

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html{{ if (isTRUE(nzchar(lang))) paste0(" lang=\"", lang, "\"") }}>
<head>
{{ headContent() }}
</head>

13
inst/template/error.html Normal file
View File

@@ -0,0 +1,13 @@
<html>
<head lang = "en">
<title>An error has occurred</title>
</head>
<body>
<h1>An error has occurred!</h1>
<p>{{message}}</p>
</body>
</html>

View File

@@ -0,0 +1 @@
.btn:focus{outline:none 2px #000}.btn:focus-visible{outline:auto 2px #000}div.active:focus{outline:none 1px #000}div.active:focus-visible{outline:auto 1px #000}a:focus{outline:none 1px #000}a:focus-visible{outline:auto 1px #000}.close:hover,.close:focus{outline:none 1px #000}.close:focus-visible{outline:auto 1px #000}.nav>li>a:hover,.nav>li>a:focus{outline:none 1px #000}.nav>li>a:focus-visible{outline:auto 1px #000}.carousel-indicators li,.carousel-indicators li.active{height:18px;width:18px;border-width:2px;position:relative;box-shadow:0px 0px 0px 1px #808080}.carousel-indicators.active li{background-color:rgba(100,149,253,0.6)}.carousel-indicators.active li.active{background-color:white}.carousel-tablist-highlight{display:block;position:absolute;outline:2px solid transparent;background-color:transparent;box-shadow:0px 0px 0px 1px transparent}.carousel-tablist-highlight.focus{outline:2px solid #6495ED;background-color:rgba(0,0,0,0.4)}a.carousel-control:focus{outline:2px solid #6495ED;background-image:linear-gradient(to right, transparent 0px, rgba(0,0,0,0.5) 100%);box-shadow:0px 0px 0px 1px #000000}.carousel-pause-button{position:absolute;top:-30em;left:-300em;display:block}.carousel-pause-button.focus{top:0.5em;left:0.5em}.carousel:hover .carousel-caption,.carousel.contrast .carousel-caption{background-color:rgba(0,0,0,0.5);z-index:10}.alert-success{color:#2d4821}.alert-info{color:#214c62}.alert-warning{color:#6c4a00;background-color:#f9f1c6}.alert-danger{color:#d2322d}.alert-danger:hover{color:#a82824}

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
/**
* @preserve HTML5 Shiv 3.7.2 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
*/
// Only run this code in IE 8
if (!!window.navigator.userAgent.match("MSIE 8")) {
!function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x<style>"+b+"</style>",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.2",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="<xyz></xyz>",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b)}(this,document);
};

View File

@@ -1,8 +0,0 @@
/*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl
* Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT
* */
// Only run this code in IE 8
if (!!window.navigator.userAgent.match("MSIE 8")) {
!function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='&shy;<style media="'+a+'"> #mq-test-1 { width: 42px; }</style>',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b<s.length;b++){var c=s[b],e=c.href,f=c.media,g=c.rel&&"stylesheet"===c.rel.toLowerCase();e&&g&&!o[e]&&(c.styleSheet&&c.styleSheet.rawCssText?(v(c.styleSheet.rawCssText,e,f),o[e]=!0):(!/^([a-zA-Z:]*\/\/)/.test(e)&&!r||e.replace(RegExp.$1,"").split("/")[0]===a.location.host)&&("//"===e.substring(0,2)&&(e=a.location.protocol+e),d.push({href:e,media:f})))}w()};x(),c.update=x,c.getEmValue=t,a.addEventListener?a.addEventListener("resize",b,!1):a.attachEvent&&a.attachEvent("onresize",b)}}(this);
};

View File

@@ -29,8 +29,8 @@ $.extend( true, DataTable.defaults, {
/* Default class modification */
$.extend( DataTable.ext.classes, {
sWrapper: "dataTables_wrapper form-inline dt-bootstrap",
sFilterInput: "form-control input-sm",
sLengthSelect: "form-control input-sm"
sFilterInput: "form-control form-control-sm input-sm",
sLengthSelect: "form-control form-control-sm input-sm"
} );

View File

@@ -1,28 +1,30 @@
/*!
* Datepicker for Bootstrap v1.6.4 (https://github.com/eternicode/bootstrap-datepicker)
*
* Copyright 2012 Stefan Petre
* Improvements by Andrew Rowls
* Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
.datepicker {
border-radius: 4px;
border-radius: 0.25rem;
direction: ltr;
}
.datepicker-inline {
width: 220px;
}
.datepicker.datepicker-rtl {
.datepicker-rtl {
direction: rtl;
}
.datepicker.datepicker-rtl table tr td span {
.datepicker-rtl.dropdown-menu {
left: auto;
}
.datepicker-rtl table tr td span {
float: right;
}
.datepicker-dropdown {
top: 0;
left: 0;
padding: 4px;
}
.datepicker-dropdown:before {
content: '';
display: inline-block;
@@ -33,6 +35,7 @@
border-bottom-color: rgba(0, 0, 0, 0.2);
position: absolute;
}
.datepicker-dropdown:after {
content: '';
display: inline-block;
@@ -42,34 +45,43 @@
border-top: 0;
position: absolute;
}
.datepicker-dropdown.datepicker-orient-left:before {
left: 6px;
}
.datepicker-dropdown.datepicker-orient-left:after {
left: 7px;
}
.datepicker-dropdown.datepicker-orient-right:before {
right: 6px;
}
.datepicker-dropdown.datepicker-orient-right:after {
right: 7px;
}
.datepicker-dropdown.datepicker-orient-bottom:before {
top: -7px;
}
.datepicker-dropdown.datepicker-orient-bottom:after {
top: -6px;
}
.datepicker-dropdown.datepicker-orient-top:before {
bottom: -7px;
border-bottom: 0;
border-top: 7px solid rgba(0, 0, 0, 0.15);
}
.datepicker-dropdown.datepicker-orient-top:after {
bottom: -6px;
border-bottom: 0;
border-top: 6px solid #fff;
}
.datepicker table {
margin: 0;
-webkit-touch-callout: none;
@@ -79,424 +91,325 @@
-ms-user-select: none;
user-select: none;
}
.datepicker table tr td,
.datepicker table tr th {
.datepicker table tr td, .datepicker table tr th {
text-align: center;
width: 30px;
height: 30px;
border-radius: 4px;
border: none;
}
.table-striped .datepicker table tr td,
.table-striped .datepicker table tr th {
.table-striped .datepicker table tr td, .table-striped .datepicker table tr th {
background-color: transparent;
}
.datepicker table tr td.old,
.datepicker table tr td.new {
color: #777777;
.datepicker table tr td.old, .datepicker table tr td.new {
color: #6c757d;
}
.datepicker table tr td.day:hover,
.datepicker table tr td.focused {
background: #eeeeee;
.datepicker table tr td.day:hover, .datepicker table tr td.focused {
color: #000;
background: #e9e9ea;
cursor: pointer;
}
.datepicker table tr td.disabled,
.datepicker table tr td.disabled:hover {
.datepicker table tr td.disabled, .datepicker table tr td.disabled:hover {
background: none;
color: #777777;
color: #6c757d;
cursor: default;
}
.datepicker table tr td.highlighted {
color: #000;
background-color: #d9edf7;
border-color: #85c5e5;
background-color: #d1ecf1;
border-color: #83ccd9;
border-radius: 0;
}
.datepicker table tr td.highlighted:focus,
.datepicker table tr td.highlighted.focus {
.datepicker table tr td.highlighted:focus, .datepicker table tr td.highlighted.focus {
color: #000;
background-color: #afd9ee;
border-color: #298fc2;
background-color: #bcd4d9;
border-color: #6299a3;
}
.datepicker table tr td.highlighted:hover {
color: #000;
background-color: #afd9ee;
border-color: #52addb;
background-color: #697679;
border-color: #73b3bf;
}
.datepicker table tr td.highlighted:active,
.datepicker table tr td.highlighted.active {
.datepicker table tr td.highlighted:active, .datepicker table tr td.highlighted.active {
color: #000;
background-color: #afd9ee;
border-color: #52addb;
background-color: #bcd4d9;
border-color: #73b3bf;
}
.datepicker table tr td.highlighted:active:hover,
.datepicker table tr td.highlighted.active:hover,
.datepicker table tr td.highlighted:active:focus,
.datepicker table tr td.highlighted.active:focus,
.datepicker table tr td.highlighted:active.focus,
.datepicker table tr td.highlighted.active.focus {
.datepicker table tr td.highlighted:active:hover, .datepicker table tr td.highlighted:active:focus, .datepicker table tr td.highlighted.focus:active, .datepicker table tr td.highlighted.active:hover, .datepicker table tr td.highlighted.active:focus, .datepicker table tr td.highlighted.active.focus {
color: #000;
background-color: #91cbe8;
border-color: #298fc2;
background-color: #adc4c8;
border-color: #6299a3;
}
.datepicker table tr td.highlighted.disabled:hover,
.datepicker table tr td.highlighted[disabled]:hover,
fieldset[disabled] .datepicker table tr td.highlighted:hover,
.datepicker table tr td.highlighted.disabled:focus,
.datepicker table tr td.highlighted[disabled]:focus,
fieldset[disabled] .datepicker table tr td.highlighted:focus,
.datepicker table tr td.highlighted.disabled.focus,
.datepicker table tr td.highlighted[disabled].focus,
fieldset[disabled] .datepicker table tr td.highlighted.focus {
background-color: #d9edf7;
border-color: #85c5e5;
.datepicker table tr td.highlighted.disabled:hover, .datepicker table tr td.highlighted.disabled:focus, .datepicker table tr td.highlighted.disabled.focus, .datepicker table tr td.highlighted[disabled]:hover, .datepicker table tr td.highlighted[disabled]:focus, .datepicker table tr td.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.highlighted:hover, fieldset[disabled] .datepicker table tr td.highlighted:focus, fieldset[disabled] .datepicker table tr td.highlighted.focus {
background-color: #d1ecf1;
border-color: #83ccd9;
}
.datepicker table tr td.highlighted.focused {
background: #afd9ee;
background: #aadce5;
}
.datepicker table tr td.highlighted.disabled,
.datepicker table tr td.highlighted.disabled:active {
background: #d9edf7;
color: #777777;
.datepicker table tr td.highlighted.disabled, .datepicker table tr td.highlighted.disabled:active {
background: #d1ecf1;
color: #6c757d;
}
.datepicker table tr td.today {
color: #000;
background-color: #ffdb99;
border-color: #ffb733;
}
.datepicker table tr td.today:focus,
.datepicker table tr td.today.focus {
.datepicker table tr td.today:focus, .datepicker table tr td.today.focus {
color: #000;
background-color: #ffc966;
border-color: #b37400;
background-color: #e6c58a;
border-color: #bf8926;
}
.datepicker table tr td.today:hover {
color: #000;
background-color: #ffc966;
border-color: #f59e00;
background-color: #806e4d;
border-color: #e0a12d;
}
.datepicker table tr td.today:active,
.datepicker table tr td.today.active {
.datepicker table tr td.today:active, .datepicker table tr td.today.active {
color: #000;
background-color: #ffc966;
border-color: #f59e00;
background-color: #e6c58a;
border-color: #e0a12d;
}
.datepicker table tr td.today:active:hover,
.datepicker table tr td.today.active:hover,
.datepicker table tr td.today:active:focus,
.datepicker table tr td.today.active:focus,
.datepicker table tr td.today:active.focus,
.datepicker table tr td.today.active.focus {
.datepicker table tr td.today:active:hover, .datepicker table tr td.today:active:focus, .datepicker table tr td.today.focus:active, .datepicker table tr td.today.active:hover, .datepicker table tr td.today.active:focus, .datepicker table tr td.today.active.focus {
color: #000;
background-color: #ffbc42;
border-color: #b37400;
background-color: #d4b67f;
border-color: #bf8926;
}
.datepicker table tr td.today.disabled:hover,
.datepicker table tr td.today[disabled]:hover,
fieldset[disabled] .datepicker table tr td.today:hover,
.datepicker table tr td.today.disabled:focus,
.datepicker table tr td.today[disabled]:focus,
fieldset[disabled] .datepicker table tr td.today:focus,
.datepicker table tr td.today.disabled.focus,
.datepicker table tr td.today[disabled].focus,
fieldset[disabled] .datepicker table tr td.today.focus {
.datepicker table tr td.today.disabled:hover, .datepicker table tr td.today.disabled:focus, .datepicker table tr td.today.disabled.focus, .datepicker table tr td.today[disabled]:hover, .datepicker table tr td.today[disabled]:focus, .datepicker table tr td.today.focus[disabled], fieldset[disabled] .datepicker table tr td.today:hover, fieldset[disabled] .datepicker table tr td.today:focus, fieldset[disabled] .datepicker table tr td.today.focus {
background-color: #ffdb99;
border-color: #ffb733;
}
.datepicker table tr td.today.focused {
background: #ffc966;
}
.datepicker table tr td.today.disabled,
.datepicker table tr td.today.disabled:active {
.datepicker table tr td.today.disabled, .datepicker table tr td.today.disabled:active {
background: #ffdb99;
color: #777777;
color: #6c757d;
}
.datepicker table tr td.range {
color: #000;
background-color: #eeeeee;
border-color: #bbbbbb;
background-color: #e9e9ea;
border-color: #b5b5b8;
border-radius: 0;
}
.datepicker table tr td.range:focus,
.datepicker table tr td.range.focus {
.datepicker table tr td.range:focus, .datepicker table tr td.range.focus {
color: #000;
background-color: #d5d5d5;
border-color: #7c7c7c;
background-color: #d2d2d3;
border-color: #88888a;
}
.datepicker table tr td.range:hover {
color: #000;
background-color: #d5d5d5;
border-color: #9d9d9d;
background-color: #757575;
border-color: #9f9fa2;
}
.datepicker table tr td.range:active,
.datepicker table tr td.range.active {
.datepicker table tr td.range:active, .datepicker table tr td.range.active {
color: #000;
background-color: #d5d5d5;
border-color: #9d9d9d;
background-color: #d2d2d3;
border-color: #9f9fa2;
}
.datepicker table tr td.range:active:hover,
.datepicker table tr td.range.active:hover,
.datepicker table tr td.range:active:focus,
.datepicker table tr td.range.active:focus,
.datepicker table tr td.range:active.focus,
.datepicker table tr td.range.active.focus {
.datepicker table tr td.range:active:hover, .datepicker table tr td.range:active:focus, .datepicker table tr td.range.focus:active, .datepicker table tr td.range.active:hover, .datepicker table tr td.range.active:focus, .datepicker table tr td.range.active.focus {
color: #000;
background-color: #c3c3c3;
border-color: #7c7c7c;
background-color: #c1c1c2;
border-color: #88888a;
}
.datepicker table tr td.range.disabled:hover,
.datepicker table tr td.range[disabled]:hover,
fieldset[disabled] .datepicker table tr td.range:hover,
.datepicker table tr td.range.disabled:focus,
.datepicker table tr td.range[disabled]:focus,
fieldset[disabled] .datepicker table tr td.range:focus,
.datepicker table tr td.range.disabled.focus,
.datepicker table tr td.range[disabled].focus,
fieldset[disabled] .datepicker table tr td.range.focus {
background-color: #eeeeee;
border-color: #bbbbbb;
.datepicker table tr td.range.disabled:hover, .datepicker table tr td.range.disabled:focus, .datepicker table tr td.range.disabled.focus, .datepicker table tr td.range[disabled]:hover, .datepicker table tr td.range[disabled]:focus, .datepicker table tr td.range.focus[disabled], fieldset[disabled] .datepicker table tr td.range:hover, fieldset[disabled] .datepicker table tr td.range:focus, fieldset[disabled] .datepicker table tr td.range.focus {
background-color: #e9e9ea;
border-color: #b5b5b8;
}
.datepicker table tr td.range.focused {
background: #d5d5d5;
background: #cfcfd1;
}
.datepicker table tr td.range.disabled,
.datepicker table tr td.range.disabled:active {
background: #eeeeee;
color: #777777;
.datepicker table tr td.range.disabled, .datepicker table tr td.range.disabled:active {
background: #e9e9ea;
color: #6c757d;
}
.datepicker table tr td.range.highlighted {
color: #000;
background-color: #e4eef3;
border-color: #9dc1d3;
background-color: #ddebee;
border-color: #99c3cc;
}
.datepicker table tr td.range.highlighted:focus,
.datepicker table tr td.range.highlighted.focus {
.datepicker table tr td.range.highlighted:focus, .datepicker table tr td.range.highlighted.focus {
color: #000;
background-color: #c1d7e3;
border-color: #4b88a6;
background-color: #c7d4d6;
border-color: #739299;
}
.datepicker table tr td.range.highlighted:hover {
color: #000;
background-color: #c1d7e3;
border-color: #73a6c0;
background-color: #6f7677;
border-color: #87acb4;
}
.datepicker table tr td.range.highlighted:active,
.datepicker table tr td.range.highlighted.active {
.datepicker table tr td.range.highlighted:active, .datepicker table tr td.range.highlighted.active {
color: #000;
background-color: #c1d7e3;
border-color: #73a6c0;
background-color: #c7d4d6;
border-color: #87acb4;
}
.datepicker table tr td.range.highlighted:active:hover,
.datepicker table tr td.range.highlighted.active:hover,
.datepicker table tr td.range.highlighted:active:focus,
.datepicker table tr td.range.highlighted.active:focus,
.datepicker table tr td.range.highlighted:active.focus,
.datepicker table tr td.range.highlighted.active.focus {
.datepicker table tr td.range.highlighted:active:hover, .datepicker table tr td.range.highlighted:active:focus, .datepicker table tr td.range.highlighted.focus:active, .datepicker table tr td.range.highlighted.active:hover, .datepicker table tr td.range.highlighted.active:focus, .datepicker table tr td.range.highlighted.active.focus {
color: #000;
background-color: #a8c8d8;
border-color: #4b88a6;
background-color: #b7c3c6;
border-color: #739299;
}
.datepicker table tr td.range.highlighted.disabled:hover,
.datepicker table tr td.range.highlighted[disabled]:hover,
fieldset[disabled] .datepicker table tr td.range.highlighted:hover,
.datepicker table tr td.range.highlighted.disabled:focus,
.datepicker table tr td.range.highlighted[disabled]:focus,
fieldset[disabled] .datepicker table tr td.range.highlighted:focus,
.datepicker table tr td.range.highlighted.disabled.focus,
.datepicker table tr td.range.highlighted[disabled].focus,
fieldset[disabled] .datepicker table tr td.range.highlighted.focus {
background-color: #e4eef3;
border-color: #9dc1d3;
.datepicker table tr td.range.highlighted.disabled:hover, .datepicker table tr td.range.highlighted.disabled:focus, .datepicker table tr td.range.highlighted.disabled.focus, .datepicker table tr td.range.highlighted[disabled]:hover, .datepicker table tr td.range.highlighted[disabled]:focus, .datepicker table tr td.range.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.range.highlighted:hover, fieldset[disabled] .datepicker table tr td.range.highlighted:focus, fieldset[disabled] .datepicker table tr td.range.highlighted.focus {
background-color: #ddebee;
border-color: #99c3cc;
}
.datepicker table tr td.range.highlighted.focused {
background: #c1d7e3;
background: #bbd7dd;
}
.datepicker table tr td.range.highlighted.disabled,
.datepicker table tr td.range.highlighted.disabled:active {
background: #e4eef3;
color: #777777;
.datepicker table tr td.range.highlighted.disabled, .datepicker table tr td.range.highlighted.disabled:active {
background: #ddebee;
color: #6c757d;
}
.datepicker table tr td.range.today {
color: #000;
background-color: #f7ca77;
border-color: #f1a417;
background-color: #f4c775;
border-color: #eca117;
}
.datepicker table tr td.range.today:focus,
.datepicker table tr td.range.today.focus {
.datepicker table tr td.range.today:focus, .datepicker table tr td.range.today.focus {
color: #000;
background-color: #f4b747;
border-color: #815608;
background-color: #dcb369;
border-color: #b17811;
}
.datepicker table tr td.range.today:hover {
color: #000;
background-color: #f4b747;
border-color: #bf800c;
background-color: #7a643b;
border-color: #d08d14;
}
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today.active {
.datepicker table tr td.range.today:active, .datepicker table tr td.range.today.active {
color: #000;
background-color: #f4b747;
border-color: #bf800c;
background-color: #dcb369;
border-color: #d08d14;
}
.datepicker table tr td.range.today:active:hover,
.datepicker table tr td.range.today.active:hover,
.datepicker table tr td.range.today:active:focus,
.datepicker table tr td.range.today.active:focus,
.datepicker table tr td.range.today:active.focus,
.datepicker table tr td.range.today.active.focus {
.datepicker table tr td.range.today:active:hover, .datepicker table tr td.range.today:active:focus, .datepicker table tr td.range.today.focus:active, .datepicker table tr td.range.today.active:hover, .datepicker table tr td.range.today.active:focus, .datepicker table tr td.range.today.active.focus {
color: #000;
background-color: #f2aa25;
border-color: #815608;
background-color: #cba561;
border-color: #b17811;
}
.datepicker table tr td.range.today.disabled:hover,
.datepicker table tr td.range.today[disabled]:hover,
fieldset[disabled] .datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today.disabled:focus,
.datepicker table tr td.range.today[disabled]:focus,
fieldset[disabled] .datepicker table tr td.range.today:focus,
.datepicker table tr td.range.today.disabled.focus,
.datepicker table tr td.range.today[disabled].focus,
fieldset[disabled] .datepicker table tr td.range.today.focus {
background-color: #f7ca77;
border-color: #f1a417;
.datepicker table tr td.range.today.disabled:hover, .datepicker table tr td.range.today.disabled:focus, .datepicker table tr td.range.today.disabled.focus, .datepicker table tr td.range.today[disabled]:hover, .datepicker table tr td.range.today[disabled]:focus, .datepicker table tr td.range.today.focus[disabled], fieldset[disabled] .datepicker table tr td.range.today:hover, fieldset[disabled] .datepicker table tr td.range.today:focus, fieldset[disabled] .datepicker table tr td.range.today.focus {
background-color: #f4c775;
border-color: #eca117;
}
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today.disabled:active {
background: #f7ca77;
color: #777777;
.datepicker table tr td.range.today.disabled, .datepicker table tr td.range.today.disabled:active {
background: #f4c775;
color: #6c757d;
}
.datepicker table tr td.selected,
.datepicker table tr td.selected.highlighted {
.datepicker table tr td.selected, .datepicker table tr td.selected.highlighted {
color: #fff;
background-color: #777777;
border-color: #555555;
background-color: #898b8d;
border-color: #6b6e71;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.selected:focus,
.datepicker table tr td.selected.highlighted:focus,
.datepicker table tr td.selected.focus,
.datepicker table tr td.selected.highlighted.focus {
.datepicker table tr td.selected:focus, .datepicker table tr td.selected.focus, .datepicker table tr td.selected.highlighted:focus, .datepicker table tr td.selected.highlighted.focus {
color: #fff;
background-color: #5e5e5e;
border-color: #161616;
background-color: #959798;
border-color: #909295;
}
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected.highlighted:hover {
.datepicker table tr td.selected:hover, .datepicker table tr td.selected.highlighted:hover {
color: #fff;
background-color: #5e5e5e;
border-color: #373737;
background-color: #c4c5c6;
border-color: #7d7f82;
}
.datepicker table tr td.selected:active,
.datepicker table tr td.selected.highlighted:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected.highlighted.active {
.datepicker table tr td.selected:active, .datepicker table tr td.selected.active, .datepicker table tr td.selected.highlighted:active, .datepicker table tr td.selected.highlighted.active {
color: #fff;
background-color: #5e5e5e;
border-color: #373737;
background-color: #959798;
border-color: #7d7f82;
}
.datepicker table tr td.selected:active:hover,
.datepicker table tr td.selected.highlighted:active:hover,
.datepicker table tr td.selected.active:hover,
.datepicker table tr td.selected.highlighted.active:hover,
.datepicker table tr td.selected:active:focus,
.datepicker table tr td.selected.highlighted:active:focus,
.datepicker table tr td.selected.active:focus,
.datepicker table tr td.selected.highlighted.active:focus,
.datepicker table tr td.selected:active.focus,
.datepicker table tr td.selected.highlighted:active.focus,
.datepicker table tr td.selected.active.focus,
.datepicker table tr td.selected.highlighted.active.focus {
.datepicker table tr td.selected:active:hover, .datepicker table tr td.selected:active:focus, .datepicker table tr td.selected.focus:active, .datepicker table tr td.selected.active:hover, .datepicker table tr td.selected.active:focus, .datepicker table tr td.selected.active.focus, .datepicker table tr td.selected.highlighted:active:hover, .datepicker table tr td.selected.highlighted:active:focus, .datepicker table tr td.selected.highlighted.focus:active, .datepicker table tr td.selected.highlighted.active:hover, .datepicker table tr td.selected.highlighted.active:focus, .datepicker table tr td.selected.highlighted.active.focus {
color: #fff;
background-color: #4c4c4c;
border-color: #161616;
background-color: #9d9fa0;
border-color: #909295;
}
.datepicker table tr td.selected.disabled:hover,
.datepicker table tr td.selected.highlighted.disabled:hover,
.datepicker table tr td.selected[disabled]:hover,
.datepicker table tr td.selected.highlighted[disabled]:hover,
fieldset[disabled] .datepicker table tr td.selected:hover,
fieldset[disabled] .datepicker table tr td.selected.highlighted:hover,
.datepicker table tr td.selected.disabled:focus,
.datepicker table tr td.selected.highlighted.disabled:focus,
.datepicker table tr td.selected[disabled]:focus,
.datepicker table tr td.selected.highlighted[disabled]:focus,
fieldset[disabled] .datepicker table tr td.selected:focus,
fieldset[disabled] .datepicker table tr td.selected.highlighted:focus,
.datepicker table tr td.selected.disabled.focus,
.datepicker table tr td.selected.highlighted.disabled.focus,
.datepicker table tr td.selected[disabled].focus,
.datepicker table tr td.selected.highlighted[disabled].focus,
fieldset[disabled] .datepicker table tr td.selected.focus,
fieldset[disabled] .datepicker table tr td.selected.highlighted.focus {
background-color: #777777;
border-color: #555555;
.datepicker table tr td.selected.disabled:hover, .datepicker table tr td.selected.disabled:focus, .datepicker table tr td.selected.disabled.focus, .datepicker table tr td.selected[disabled]:hover, .datepicker table tr td.selected[disabled]:focus, .datepicker table tr td.selected.focus[disabled], fieldset[disabled] .datepicker table tr td.selected:hover, fieldset[disabled] .datepicker table tr td.selected:focus, fieldset[disabled] .datepicker table tr td.selected.focus, .datepicker table tr td.selected.highlighted.disabled:hover, .datepicker table tr td.selected.highlighted.disabled:focus, .datepicker table tr td.selected.highlighted.disabled.focus, .datepicker table tr td.selected.highlighted[disabled]:hover, .datepicker table tr td.selected.highlighted[disabled]:focus, .datepicker table tr td.selected.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.selected.highlighted:hover, fieldset[disabled] .datepicker table tr td.selected.highlighted:focus, fieldset[disabled] .datepicker table tr td.selected.highlighted.focus {
background-color: #898b8d;
border-color: #6b6e71;
}
.datepicker table tr td.active,
.datepicker table tr td.active.highlighted {
.datepicker table tr td.active, .datepicker table tr td.active.highlighted {
color: #fff;
background-color: #337ab7;
border-color: #2e6da4;
background-color: #007bff;
border-color: #0277f4;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.active:focus,
.datepicker table tr td.active.highlighted:focus,
.datepicker table tr td.active.focus,
.datepicker table tr td.active.highlighted.focus {
.datepicker table tr td.active:focus, .datepicker table tr td.active.focus, .datepicker table tr td.active.highlighted:focus, .datepicker table tr td.active.highlighted.focus {
color: #fff;
background-color: #286090;
border-color: #122b40;
background-color: #1a88ff;
border-color: #4199f7;
}
.datepicker table tr td.active:hover,
.datepicker table tr td.active.highlighted:hover {
.datepicker table tr td.active:hover, .datepicker table tr td.active.highlighted:hover {
color: #fff;
background-color: #286090;
border-color: #204d74;
background-color: #80bdff;
border-color: #2087f5;
}
.datepicker table tr td.active:active,
.datepicker table tr td.active.highlighted:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active.highlighted.active {
.datepicker table tr td.active:active, .datepicker table tr td.active.active, .datepicker table tr td.active.highlighted:active, .datepicker table tr td.active.highlighted.active {
color: #fff;
background-color: #286090;
border-color: #204d74;
background-color: #1a88ff;
border-color: #2087f5;
}
.datepicker table tr td.active:active:hover,
.datepicker table tr td.active.highlighted:active:hover,
.datepicker table tr td.active.active:hover,
.datepicker table tr td.active.highlighted.active:hover,
.datepicker table tr td.active:active:focus,
.datepicker table tr td.active.highlighted:active:focus,
.datepicker table tr td.active.active:focus,
.datepicker table tr td.active.highlighted.active:focus,
.datepicker table tr td.active:active.focus,
.datepicker table tr td.active.highlighted:active.focus,
.datepicker table tr td.active.active.focus,
.datepicker table tr td.active.highlighted.active.focus {
.datepicker table tr td.active:active:hover, .datepicker table tr td.active:active:focus, .datepicker table tr td.active.focus:active, .datepicker table tr td.active.active:hover, .datepicker table tr td.active.active:focus, .datepicker table tr td.active.active.focus, .datepicker table tr td.active.highlighted:active:hover, .datepicker table tr td.active.highlighted:active:focus, .datepicker table tr td.active.highlighted.focus:active, .datepicker table tr td.active.highlighted.active:hover, .datepicker table tr td.active.highlighted.active:focus, .datepicker table tr td.active.highlighted.active.focus {
color: #fff;
background-color: #204d74;
border-color: #122b40;
background-color: #2b91ff;
border-color: #4199f7;
}
.datepicker table tr td.active.disabled:hover,
.datepicker table tr td.active.highlighted.disabled:hover,
.datepicker table tr td.active[disabled]:hover,
.datepicker table tr td.active.highlighted[disabled]:hover,
fieldset[disabled] .datepicker table tr td.active:hover,
fieldset[disabled] .datepicker table tr td.active.highlighted:hover,
.datepicker table tr td.active.disabled:focus,
.datepicker table tr td.active.highlighted.disabled:focus,
.datepicker table tr td.active[disabled]:focus,
.datepicker table tr td.active.highlighted[disabled]:focus,
fieldset[disabled] .datepicker table tr td.active:focus,
fieldset[disabled] .datepicker table tr td.active.highlighted:focus,
.datepicker table tr td.active.disabled.focus,
.datepicker table tr td.active.highlighted.disabled.focus,
.datepicker table tr td.active[disabled].focus,
.datepicker table tr td.active.highlighted[disabled].focus,
fieldset[disabled] .datepicker table tr td.active.focus,
fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
background-color: #337ab7;
border-color: #2e6da4;
.datepicker table tr td.active.disabled:hover, .datepicker table tr td.active.disabled:focus, .datepicker table tr td.active.disabled.focus, .datepicker table tr td.active[disabled]:hover, .datepicker table tr td.active[disabled]:focus, .datepicker table tr td.active.focus[disabled], fieldset[disabled] .datepicker table tr td.active:hover, fieldset[disabled] .datepicker table tr td.active:focus, fieldset[disabled] .datepicker table tr td.active.focus, .datepicker table tr td.active.highlighted.disabled:hover, .datepicker table tr td.active.highlighted.disabled:focus, .datepicker table tr td.active.highlighted.disabled.focus, .datepicker table tr td.active.highlighted[disabled]:hover, .datepicker table tr td.active.highlighted[disabled]:focus, .datepicker table tr td.active.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.active.highlighted:hover, fieldset[disabled] .datepicker table tr td.active.highlighted:focus, fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
background-color: #007bff;
border-color: #0277f4;
}
.datepicker table tr td span {
display: block;
width: 23%;
@@ -507,172 +420,114 @@ fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
cursor: pointer;
border-radius: 4px;
}
.datepicker table tr td span:hover,
.datepicker table tr td span.focused {
background: #eeeeee;
.datepicker table tr td span:hover, .datepicker table tr td span.focused {
color: #000;
background: #e9e9ea;
}
.datepicker table tr td span.disabled,
.datepicker table tr td span.disabled:hover {
.datepicker table tr td span.disabled, .datepicker table tr td span.disabled:hover {
background: none;
color: #777777;
color: #6c757d;
cursor: default;
}
.datepicker table tr td span.active,
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active.disabled:hover {
.datepicker table tr td span.active, .datepicker table tr td span.active:hover, .datepicker table tr td span.active.disabled, .datepicker table tr td span.active.disabled:hover {
color: #fff;
background-color: #337ab7;
border-color: #2e6da4;
background-color: #007bff;
border-color: #0277f4;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td span.active:focus,
.datepicker table tr td span.active:hover:focus,
.datepicker table tr td span.active.disabled:focus,
.datepicker table tr td span.active.disabled:hover:focus,
.datepicker table tr td span.active.focus,
.datepicker table tr td span.active:hover.focus,
.datepicker table tr td span.active.disabled.focus,
.datepicker table tr td span.active.disabled:hover.focus {
.datepicker table tr td span.active:focus, .datepicker table tr td span.active.focus, .datepicker table tr td span.active:hover:focus, .datepicker table tr td span.active.focus:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled.focus:hover {
color: #fff;
background-color: #286090;
border-color: #122b40;
background-color: #1a88ff;
border-color: #4199f7;
}
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active:hover:hover,
.datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active.disabled:hover:hover {
.datepicker table tr td span.active:hover, .datepicker table tr td span.active:hover:hover, .datepicker table tr td span.active.disabled:hover, .datepicker table tr td span.active.disabled:hover:hover {
color: #fff;
background-color: #286090;
border-color: #204d74;
background-color: #80bdff;
border-color: #2087f5;
}
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active {
.datepicker table tr td span.active:active, .datepicker table tr td span.active.active, .datepicker table tr td span.active:hover:active, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.disabled:active, .datepicker table tr td span.active.disabled.active, .datepicker table tr td span.active.disabled:hover:active, .datepicker table tr td span.active.disabled.active:hover {
color: #fff;
background-color: #286090;
border-color: #204d74;
background-color: #1a88ff;
border-color: #2087f5;
}
.datepicker table tr td span.active:active:hover,
.datepicker table tr td span.active:hover:active:hover,
.datepicker table tr td span.active.disabled:active:hover,
.datepicker table tr td span.active.disabled:hover:active:hover,
.datepicker table tr td span.active.active:hover,
.datepicker table tr td span.active:hover.active:hover,
.datepicker table tr td span.active.disabled.active:hover,
.datepicker table tr td span.active.disabled:hover.active:hover,
.datepicker table tr td span.active:active:focus,
.datepicker table tr td span.active:hover:active:focus,
.datepicker table tr td span.active.disabled:active:focus,
.datepicker table tr td span.active.disabled:hover:active:focus,
.datepicker table tr td span.active.active:focus,
.datepicker table tr td span.active:hover.active:focus,
.datepicker table tr td span.active.disabled.active:focus,
.datepicker table tr td span.active.disabled:hover.active:focus,
.datepicker table tr td span.active:active.focus,
.datepicker table tr td span.active:hover:active.focus,
.datepicker table tr td span.active.disabled:active.focus,
.datepicker table tr td span.active.disabled:hover:active.focus,
.datepicker table tr td span.active.active.focus,
.datepicker table tr td span.active:hover.active.focus,
.datepicker table tr td span.active.disabled.active.focus,
.datepicker table tr td span.active.disabled:hover.active.focus {
.datepicker table tr td span.active:active:hover, .datepicker table tr td span.active:active:focus, .datepicker table tr td span.active.focus:active, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.active:focus, .datepicker table tr td span.active.active.focus, .datepicker table tr td span.active:hover:active:hover, .datepicker table tr td span.active:hover:active:focus, .datepicker table tr td span.active.focus:hover:active, .datepicker table tr td span.active.active:hover:hover, .datepicker table tr td span.active.active:hover:focus, .datepicker table tr td span.active.active.focus:hover, .datepicker table tr td span.active.disabled:active:hover, .datepicker table tr td span.active.disabled:active:focus, .datepicker table tr td span.active.disabled.focus:active, .datepicker table tr td span.active.disabled.active:hover, .datepicker table tr td span.active.disabled.active:focus, .datepicker table tr td span.active.disabled.active.focus, .datepicker table tr td span.active.disabled:hover:active:hover, .datepicker table tr td span.active.disabled:hover:active:focus, .datepicker table tr td span.active.disabled.focus:hover:active, .datepicker table tr td span.active.disabled.active:hover:hover, .datepicker table tr td span.active.disabled.active:hover:focus, .datepicker table tr td span.active.disabled.active.focus:hover {
color: #fff;
background-color: #204d74;
border-color: #122b40;
background-color: #2b91ff;
border-color: #4199f7;
}
.datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active:hover.disabled:hover,
.datepicker table tr td span.active.disabled.disabled:hover,
.datepicker table tr td span.active.disabled:hover.disabled:hover,
.datepicker table tr td span.active[disabled]:hover,
.datepicker table tr td span.active:hover[disabled]:hover,
.datepicker table tr td span.active.disabled[disabled]:hover,
.datepicker table tr td span.active.disabled:hover[disabled]:hover,
fieldset[disabled] .datepicker table tr td span.active:hover,
fieldset[disabled] .datepicker table tr td span.active:hover:hover,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover,
.datepicker table tr td span.active.disabled:focus,
.datepicker table tr td span.active:hover.disabled:focus,
.datepicker table tr td span.active.disabled.disabled:focus,
.datepicker table tr td span.active.disabled:hover.disabled:focus,
.datepicker table tr td span.active[disabled]:focus,
.datepicker table tr td span.active:hover[disabled]:focus,
.datepicker table tr td span.active.disabled[disabled]:focus,
.datepicker table tr td span.active.disabled:hover[disabled]:focus,
fieldset[disabled] .datepicker table tr td span.active:focus,
fieldset[disabled] .datepicker table tr td span.active:hover:focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus,
.datepicker table tr td span.active.disabled.focus,
.datepicker table tr td span.active:hover.disabled.focus,
.datepicker table tr td span.active.disabled.disabled.focus,
.datepicker table tr td span.active.disabled:hover.disabled.focus,
.datepicker table tr td span.active[disabled].focus,
.datepicker table tr td span.active:hover[disabled].focus,
.datepicker table tr td span.active.disabled[disabled].focus,
.datepicker table tr td span.active.disabled:hover[disabled].focus,
fieldset[disabled] .datepicker table tr td span.active.focus,
fieldset[disabled] .datepicker table tr td span.active:hover.focus,
fieldset[disabled] .datepicker table tr td span.active.disabled.focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover.focus {
background-color: #337ab7;
border-color: #2e6da4;
.datepicker table tr td span.active.disabled:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active[disabled]:hover, .datepicker table tr td span.active[disabled]:focus, .datepicker table tr td span.active.focus[disabled], fieldset[disabled] .datepicker table tr td span.active:hover, fieldset[disabled] .datepicker table tr td span.active:focus, fieldset[disabled] .datepicker table tr td span.active.focus, .datepicker table tr td span.active.disabled:hover:hover, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled.focus:hover, .datepicker table tr td span.active[disabled]:hover:hover, .datepicker table tr td span.active[disabled]:hover:focus, .datepicker table tr td span.active.focus[disabled]:hover, fieldset[disabled] .datepicker table tr td span.active:hover:hover, fieldset[disabled] .datepicker table tr td span.active:hover:focus, fieldset[disabled] .datepicker table tr td span.active.focus:hover, .datepicker table tr td span.active.disabled.disabled:hover, .datepicker table tr td span.active.disabled.disabled:focus, .datepicker table tr td span.active.disabled.disabled.focus, .datepicker table tr td span.active.disabled[disabled]:hover, .datepicker table tr td span.active.disabled[disabled]:focus, .datepicker table tr td span.active.disabled.focus[disabled], fieldset[disabled] .datepicker table tr td span.active.disabled:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:focus, fieldset[disabled] .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled.disabled:hover:hover, .datepicker table tr td span.active.disabled.disabled:hover:focus, .datepicker table tr td span.active.disabled.disabled.focus:hover, .datepicker table tr td span.active.disabled[disabled]:hover:hover, .datepicker table tr td span.active.disabled[disabled]:hover:focus, .datepicker table tr td span.active.disabled.focus[disabled]:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus, fieldset[disabled] .datepicker table tr td span.active.disabled.focus:hover {
background-color: #007bff;
border-color: #0277f4;
}
.datepicker table tr td span.old,
.datepicker table tr td span.new {
color: #777777;
.datepicker table tr td span.old, .datepicker table tr td span.new {
color: #6c757d;
}
.datepicker .datepicker-switch {
width: 145px;
}
.datepicker .datepicker-switch,
.datepicker .prev,
.datepicker .next,
.datepicker tfoot tr th {
cursor: pointer;
}
.datepicker .datepicker-switch:hover,
.datepicker .prev:hover,
.datepicker .next:hover,
.datepicker tfoot tr th:hover {
background: #eeeeee;
color: #000;
background: #e9e9ea;
}
.datepicker .prev.disabled, .datepicker .next.disabled {
visibility: hidden;
}
.datepicker .cw {
font-size: 10px;
width: 12px;
padding: 0 2px 0 5px;
vertical-align: middle;
}
.input-group.date .input-group-addon {
cursor: pointer;
}
.input-daterange {
width: 100%;
}
.input-daterange input {
text-align: center;
}
.input-daterange input:first-child {
border-radius: 3px 0 0 3px;
}
.input-daterange input:last-child {
border-radius: 0 3px 3px 0;
}
.input-daterange .input-group-addon {
width: auto;
min-width: 16px;
padding: 4px 5px;
line-height: 1.42857143;
text-shadow: 0 1px 0 #fff;
line-height: 1.5;
border-width: 1px 0;
margin-left: -5px;
margin-right: -5px;
}
/*# sourceMappingURL=bootstrap-datepicker3.css.map */

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