Compare commits

...

899 Commits

Author SHA1 Message Date
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
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
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
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
427 changed files with 46775 additions and 18262 deletions

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 cask install 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

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

@@ -0,0 +1,147 @@
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', 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: '12.x'
# https://github.com/actions/cache/blame/ccf96194800dbb7b7094edcd5a7cf3ec3c270f10/examples.md#L185-L200
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: yarn cache
uses: actions/cache@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 }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ matrix.config.os }}-yarn-
- name: Build JS
run: |
cd tools
yarn install --frozen-lockfile
yarn build
git add ../inst
git commit -m 'yarn build (GitHub Actions)' || echo "No yarn changes to commit"
- name: Check node build
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"

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
script: ./tools/checkDocsCurrent.sh
- name: "Javascript check"
language: node_js
cache: yarn
script: ./tools/checkJSCurrent.sh
node_js:
- "12"
- r: 3.2
- r: 3.3
- r: 3.4
- r: 3.5
- r: release
- r: devel
sudo: false
cache: packages
notifications:
email:
on_success: change
on_failure: change
slack:
on_success: change
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.9001
Version: 1.6.0
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,17 +80,23 @@ Imports:
mime (>= 0.3),
jsonlite (>= 0.9.16),
xtable,
digest,
htmltools (>= 0.4.0.9001),
digest (>= 0.6.25),
htmltools (>= 0.5.0.9001),
R6 (>= 2.0),
sourcetools,
later (>= 1.0.0),
promises (>= 1.1.0),
tools,
crayon,
rlang (>= 0.4.0),
rlang (>= 0.4.9),
fastmap (>= 1.0.0),
withr
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),
@@ -90,30 +107,33 @@ Suggests:
ggplot2,
reactlog (>= 1.0.0),
magrittr,
shinytest,
shinytest (>= 1.4.0.9003),
yaml,
future,
dygraphs
Remotes:
rstudio/htmltools
URL: http://shiny.rstudio.com
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'
@@ -123,7 +143,6 @@ Collate:
'history.R'
'hooks.R'
'html-deps.R'
'htmltools.R'
'image-interact-opts.R'
'image-interact.R'
'imageutils.R'
@@ -145,9 +164,11 @@ 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'
@@ -155,24 +176,31 @@ Collate:
'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'
'test-module.R'
'test-server.R'
'test.R'
'update-input.R'
RoxygenNote: 7.0.2
'utils-lang.R'
'viewer.R'
RoxygenNote: 7.1.1
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RdMacros: lifecycle

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

105
NAMESPACE
View File

@@ -25,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)
@@ -32,6 +48,7 @@ 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..)
@@ -49,6 +66,8 @@ export(animationOptions)
export(appendTab)
export(as.shiny.appobj)
export(basicPage)
export(bindCache)
export(bindEvent)
export(bookmarkButton)
export(bootstrapLib)
export(bootstrapPage)
@@ -72,6 +91,7 @@ export(dateInput)
export(dateRangeInput)
export(dblclickOpts)
export(debounce)
export(devmode)
export(dialogViewer)
export(diskCache)
export(div)
@@ -98,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)
@@ -115,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)
@@ -142,6 +166,7 @@ export(loadSupport)
export(mainPanel)
export(makeReactiveBinding)
export(markRenderFunction)
export(markdown)
export(maskReactiveContext)
export(memoryCache)
export(modalButton)
@@ -177,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)
@@ -194,6 +221,7 @@ export(reactlog)
export(reactlogReset)
export(reactlogShow)
export(registerInputHandler)
export(registerThemeDependency)
export(removeInputHandler)
export(removeModal)
export(removeNotification)
@@ -229,6 +257,7 @@ export(setSerializer)
export(shinyApp)
export(shinyAppDir)
export(shinyAppFile)
export(shinyAppTemplate)
export(shinyOptions)
export(shinyServer)
export(shinyUI)
@@ -252,6 +281,7 @@ export(strong)
export(submitButton)
export(suppressDependencies)
export(tabPanel)
export(tabPanelBody)
export(tableOutput)
export(tabsetPanel)
export(tag)
@@ -263,7 +293,6 @@ export(tagHasAttribute)
export(tagList)
export(tagSetChildren)
export(tags)
export(testModule)
export(testServer)
export(textAreaInput)
export(textInput)
@@ -272,6 +301,7 @@ export(throttle)
export(titlePanel)
export(uiOutput)
export(updateActionButton)
export(updateActionLink)
export(updateCheckboxGroupInput)
export(updateCheckboxInput)
export(updateDateInput)
@@ -302,18 +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(digest,digest)
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(withr,with_options)
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,maybe_missing)
importFrom(rlang,missing_arg)
importFrom(rlang,new_function)
importFrom(rlang,new_quosure)
importFrom(rlang,pairlist2)
importFrom(rlang,quo)
importFrom(rlang,zap_srcref)

632
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()
}

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

@@ -0,0 +1,757 @@
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, 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)
#' )
#' }
#' ```
#'
#'
#' @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 <- digest(extractCacheHint(x), algo = "spookyhash")
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 <- digest(extractCacheHint(x), algo = "spookyhash")
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 <- digest(list(cacheKeyResult, cacheHint), algo = "spookyhash")
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
}

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

@@ -0,0 +1,308 @@
#' 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.
#'
#' @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. (Note that "recalculate a value" does not
#' generally count as performing an action -- use [reactive()] for that.) The
#' first argument is observer whose code should be executed whenever the event
#' occurs.
#'
#' 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.
#'
#' `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

@@ -79,7 +79,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 +217,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 +296,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()) {
@@ -453,7 +469,7 @@ hasCurrentRestoreContext <- function() {
domain <- getDefaultReactiveDomain()
if (!is.null(domain) && !is.null(domain$restoreContext))
return(TRUE)
return(FALSE)
}

View File

@@ -16,6 +16,7 @@
#' @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 +26,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 +88,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)
}
@@ -118,6 +120,7 @@ fluidRow <- function(...) {
#' @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 +131,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 +159,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
@@ -355,6 +359,8 @@ sidebarLayout <- function(sidebarPanel,
sidebarPanel <- function(..., width = 4) {
div(class=paste0("col-sm-", width),
tags$form(class="well",
# A11y semantic landmark for sidebar
role="complementary",
...
)
)
@@ -364,6 +370,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",
...
)
}
@@ -432,7 +440,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]
@@ -515,7 +523,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)
@@ -693,37 +701,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)))
}

View File

@@ -4,7 +4,7 @@ NULL
#' Create a Bootstrap page
#'
#' Create a Shiny UI page that loads the CSS and JavaScript for
#' [Bootstrap](http://getbootstrap.com/), and has no content in the page
#' [Bootstrap](https://getbootstrap.com/), and has no content in the page
#' body (other than what you provide).
#'
#' This function is primarily intended for users who are proficient in HTML/CSS,
@@ -16,8 +16,15 @@ NULL
#' @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, e.g. `www/bootstrap.css`)
#' @param theme One of the following:
#' * `NULL` (the default), which implies a "stock" build of Bootstrap 3.
#' * A [bslib::bs_theme()] object. This can be used to replace a stock
#' build of Bootstrap 3 with a customized version of Bootstrap 3 or higher.
#' * A character string pointing to an alternative Bootstrap stylesheet
#' (normally a css file within the www directory, e.g. `www/bootstrap.css`).
#' @param lang ISO 639-1 language code for the HTML page, such as "en" or "ko".
#' This will be used as the lang in the \code{<html>} tag, as in \code{<html lang="en">}.
#' The default (NULL) results in an empty string.
#'
#' @return A UI defintion that can be passed to the [shinyUI] function.
#'
@@ -26,56 +33,204 @@ NULL
#'
#' @seealso [fluidPage()], [fixedPage()]
#' @export
bootstrapPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
bootstrapPage <- function(..., title = NULL, responsive = deprecated(), theme = NULL, lang = NULL) {
if (!is.null(responsive)) {
shinyDeprecated("The 'responsive' argument is no longer used with Bootstrap 3.")
if (lifecycle::is_present(responsive)) {
shinyDeprecated(
"0.10.2.2", "bootstrapPage(responsive=)",
details = "The 'responsive' argument is no longer used with the latest version of Bootstrap."
)
}
attachDependencies(
tagList(
if (!is.null(title)) tags$head(tags$title(title)),
if (!is.null(theme)) {
tags$head(tags$link(rel="stylesheet", type="text/css", href = theme))
},
# remainder of tags passed to the function
list(...)
),
bootstrapLib()
args <- list(
if (!is.null(title)) tags$head(tags$title(title)),
if (is.character(theme)) {
if (length(theme) > 1) stop("`theme` must point to a single CSS file, not multiple files.")
tags$head(tags$link(rel="stylesheet", type="text/css", href=theme))
},
# remainder of tags passed to the function
list(...)
)
# If theme is a bslib::bs_theme() object, bootstrapLib() needs to come first
# (so other tags, when rendered via tagFunction(), know about the relevant
# theme). However, if theme is anything else, we intentionally avoid changing
# the tagList() contents to avoid breaking user code that makes assumptions
# about the return value https://github.com/rstudio/shiny/issues/3235
if (is_bs_theme(theme)) {
args <- c(bootstrapLib(theme), args)
ui <- do.call(tagList, args)
} else {
ui <- do.call(tagList, args)
ui <- attachDependencies(ui, bootstrapLib())
}
setLang(ui, lang)
}
setLang <- function(ui, lang) {
# Add lang attribute to be passed to renderPage function
attr(ui, "lang") <- lang
ui
}
getLang <- function(ui) {
# Check if ui has lang attribute; otherwise, NULL
attr(ui, "lang", exact = TRUE)
}
#' Bootstrap libraries
#'
#' This function returns a set of web dependencies necessary for using Bootstrap
#' This function defines a set of web dependencies necessary for using Bootstrap
#' components in a web page.
#'
#' It isn't necessary to call this function if you use
#' [bootstrapPage()] or others which use `bootstrapPage`, such
#' [basicPage()], [fluidPage()], [fillPage()],
#' [pageWithSidebar()], and [navbarPage()], because they
#' already include the Bootstrap web dependencies.
#' It isn't necessary to call this function if you use [bootstrapPage()] or
#' others which use `bootstrapPage`, such [fluidPage()], [navbarPage()],
#' [fillPage()], etc, because they already include the Bootstrap web dependencies.
#'
#' @inheritParams bootstrapPage
#' @export
bootstrapLib <- function(theme = NULL) {
htmlDependency("bootstrap", "3.4.1",
tagFunction(function() {
# If we're not compiling Bootstrap Sass (from bslib), return the
# static Bootstrap build.
if (!is_bs_theme(theme)) {
# We'll enter here if `theme` is the path to a .css file, like that
# provided by `shinythemes::shinytheme("darkly")`.
return(bootstrapDependency(theme))
}
# Make bootstrap Sass available so other tagFunction()s (e.g.,
# sliderInput() et al) can resolve their HTML dependencies at render time
# using getCurrentTheme(). Note that we're making an implicit assumption
# that this tagFunction() executes *before* all other tagFunction()s; but
# that should be fine considering that, DOM tree order is preorder,
# depth-first traversal, and at least in the bootstrapPage(theme) case, we
# have control over the relative ordering.
# https://dom.spec.whatwg.org/#concept-tree
# https://stackoverflow.com/a/16113998/1583084
#
# Note also that since this is shinyOptions() (and not options()), the
# option is automatically reset when the app (or session) exits
if (isRunning()) {
setCurrentTheme(theme)
registerThemeDependency(bs_theme_deps)
} else {
# Technically, this a potential issue (someone trying to execute/render
# bootstrapLib outside of a Shiny app), but it seems that, in that case,
# you likely have other problems, since sliderInput() et al. already assume
# that Shiny is the one doing the rendering
#warning(
# "It appears `shiny::bootstrapLib()` was rendered outside of an Shiny ",
# "application context, likely by calling `as.tags()`, `as.character()`, ",
# "or `print()` directly on `bootstrapLib()` or UI components that may ",
# "depend on it (e.g., `fluidPage()`, etc). For 'themable' UI components ",
# "(e.g., `sliderInput()`, `selectInput()`, `dateInput()`, etc) to style ",
# "themselves based on the Bootstrap theme, make sure `bootstrapLib()` is ",
# "provided directly to the UI and that the UI is provided direction to ",
# "`shinyApp()` (or `runApp()`)", call. = FALSE
#)
}
bslib::bs_theme_dependencies(theme)
})
}
# This is defined outside of bootstrapLib() because registerThemeDependency()
# wants a non-anonymous function with a single argument
bs_theme_deps <- function(theme) {
bslib::bs_theme_dependencies(theme)
}
is_bs_theme <- function(x) {
is_available("bslib", "0.2.0.9000") &&
bslib::is_bs_theme(x)
}
#' Obtain Shiny's Bootstrap Sass theme
#'
#' Intended for use by Shiny developers to create Shiny bindings with intelligent
#' styling based on the [bootstrapLib()]'s `theme` value.
#'
#' @return If called at render-time (i.e., inside a [htmltools::tagFunction()]),
#' and [bootstrapLib()]'s `theme` has been set to a [bslib::bs_theme()]
#' object, then this returns the `theme`. Otherwise, this returns `NULL`.
#' @seealso [getCurrentOutputInfo()], [bootstrapLib()], [htmltools::tagFunction()]
#'
#' @keywords internal
#' @export
getCurrentTheme <- function() {
getShinyOption("bootstrapTheme", default = NULL)
}
setCurrentTheme <- function(theme) {
shinyOptions(bootstrapTheme = theme)
}
#' Register a theme dependency
#'
#' This function registers a function that returns an [htmlDependency()] or list
#' of such objects. If `session$setCurrentTheme()` is called, the function will
#' be re-executed, and the resulting html dependency will be sent to the client.
#'
#' Note that `func` should **not** be an anonymous function, or a function which
#' is defined within the calling function. This is so that,
#' `registerThemeDependency()` is called multiple times with the function, it
#' tries to deduplicate them
#'
#' @param func A function that takes one argument, `theme` (which is a
#' [sass::sass_layer()] object), and returns an htmlDependency object, or list
#' of them.
#'
#' @export
#' @keywords internal
registerThemeDependency <- function(func) {
func_expr <- substitute(func)
if (is.call(func_expr) && identical(func_expr[[1]], as.symbol("function"))) {
warning("`func` should not be an anonymous function. ",
"It should be declared outside of the function that calls registerThemeDependency(); ",
"otherwise it will not be deduplicated by Shiny and multiple copies of the ",
"resulting htmlDependency may be computed and sent to the client.")
}
if (!is.function(func) || length(formals(func)) != 1) {
stop("`func` must be a function with one argument (the current theme)")
}
# Note that this will automatically scope to the app or session level,
# depending on if this is called from within a session or not.
funcs <- getShinyOption("themeDependencyFuncs", default = list())
# Don't add func if it's already present.
have_func <- any(vapply(funcs, identical, logical(1), func))
if (!have_func) {
funcs[[length(funcs) + 1]] <- func
}
shinyOptions("themeDependencyFuncs" = funcs)
}
bootstrapDependency <- function(theme) {
htmlDependency(
"bootstrap", "3.4.1",
c(
href = "shared/bootstrap",
file = system.file("www/shared/bootstrap", package = "shiny")
),
script = c(
"js/bootstrap.min.js",
# These shims are necessary for IE 8 compatibility
"shim/html5shiv.min.js",
"shim/respond.min.js"
# Safely adding accessibility plugin for screen readers and keyboard users; no break for sighted aspects (see https://github.com/paypal/bootstrap-accessibility-plugin)
"accessibility/js/bootstrap-accessibility.min.js"
),
stylesheet = c(
theme %||% "css/bootstrap.min.css",
# Safely adding accessibility plugin for screen readers and keyboard users; no break for sighted aspects (see https://github.com/paypal/bootstrap-accessibility-plugin)
"accessibility/css/bootstrap-accessibility.min.css"
),
stylesheet = if (is.null(theme)) "css/bootstrap.min.css",
meta = list(viewport = "width=device-width, initial-scale=1")
)
}
#' @rdname bootstrapPage
#' @export
basicPage <- function(...) {
@@ -129,6 +284,7 @@ basicPage <- function(...) {
#' shown in the document).
#' @param bootstrap If `TRUE`, load the Bootstrap CSS library.
#' @param theme URL to alternative Bootstrap stylesheet.
#' @inheritParams bootstrapPage
#'
#' @family layout functions
#'
@@ -156,7 +312,7 @@ basicPage <- function(...) {
#' )
#' @export
fillPage <- function(..., padding = 0, title = NULL, bootstrap = TRUE,
theme = NULL) {
theme = NULL, lang = NULL) {
fillCSS <- tags$head(tags$style(type = "text/css",
"html, body { width: 100%; height: 100%; overflow: hidden; }",
@@ -164,14 +320,18 @@ fillPage <- function(..., padding = 0, title = NULL, bootstrap = TRUE,
))
if (isTRUE(bootstrap)) {
bootstrapPage(title = title, theme = theme, fillCSS, ...)
ui <- bootstrapPage(title = title, theme = theme, fillCSS, lang = lang, ...)
} else {
tagList(
ui <- tagList(
fillCSS,
if (!is.null(title)) tags$head(tags$title(title)),
...
)
ui <- setLang(ui, lang)
}
return(ui)
}
collapseSizes <- function(padding) {
@@ -223,6 +383,7 @@ collapseSizes <- function(padding) {
#' `www/bootstrap.css` you would use `theme = "bootstrap.css"`.
#' @param windowTitle The title that should be displayed by the browser window.
#' Useful if `title` is not a string.
#' @inheritParams bootstrapPage
#' @param icon Optional icon to appear on a `navbarMenu` tab.
#'
#' @return A UI defintion that can be passed to the [shinyUI] function.
@@ -263,14 +424,15 @@ navbarPage <- function(title,
footer = NULL,
inverse = FALSE,
collapsible = FALSE,
collapsable,
collapsable = deprecated(),
fluid = TRUE,
responsive = NULL,
responsive = deprecated(),
theme = NULL,
windowTitle = title) {
windowTitle = title,
lang = NULL) {
if (!missing(collapsable)) {
shinyDeprecated("`collapsable` is deprecated; use `collapsible` instead.")
if (lifecycle::is_present(collapsable)) {
shinyDeprecated("0.10.2.2", "navbarPage(collapsable =)", "navbarPage(collapsible =)")
collapsible <- collapsable
}
@@ -338,6 +500,7 @@ navbarPage <- function(title,
title = windowTitle,
responsive = responsive,
theme = theme,
lang = lang,
tags$nav(class=navbarClass, role="navigation", containerDiv),
contentDiv
)
@@ -464,14 +627,12 @@ helpText <- function(...) {
#' Create a tab panel
#'
#' Create a tab panel that can be included within a [tabsetPanel()] or
#' a [navbarPage()].
#'
#' @param title Display title for tab
#' @param ... UI elements to include within the tab
#' @param value The value that should be sent when `tabsetPanel` reports
#' that this tab is selected. If omitted and `tabsetPanel` has an
#' `id`, then the title will be used..
#' `id`, then the title will be used.
#' @param icon Optional icon to appear on the tab. This attribute is only
#' valid when using a `tabPanel` within a [navbarPage()].
#' @return A tab that can be passed to [tabsetPanel()]
@@ -489,12 +650,29 @@ helpText <- function(...) {
#' )
#' )
#' @export
#' @describeIn tabPanel Create a tab panel that can be included within a [tabsetPanel()] or a [navbarPage()].
tabPanel <- function(title, ..., value = title, icon = NULL) {
divTag <- div(class="tab-pane",
title=title,
`data-value`=value,
`data-icon-class` = iconClass(icon),
...)
div(
class = "tab-pane",
title = title,
`data-value` = value,
`data-icon-class` = iconClass(icon),
...
)
}
#' @export
#' @describeIn tabPanel Create a tab panel that drops the title argument.
#' This function should be used within `tabsetPanel(type = "hidden")`. See [tabsetPanel()] for example usage.
tabPanelBody <- function(value, ..., icon = NULL) {
if (
!is.character(value) ||
length(value) != 1 ||
any(is.na(value)) ||
nchar(value) == 0
) {
stop("`value` must be a single, non-empty string value")
}
tabPanel(title = NULL, ..., value = value, icon = icon)
}
#' Create a tabset panel
@@ -510,8 +688,13 @@ tabPanel <- function(title, ..., value = title, icon = NULL) {
#' @param selected The `value` (or, if none was supplied, the `title`)
#' of the tab that should be selected by default. If `NULL`, the first
#' tab will be selected.
#' @param type Use "tabs" for the standard look; Use "pills" for a more plain
#' look where tabs are selected using a background fill color.
#' @param type \describe{
#' \item{`"tabs"`}{Standard tab look}
#' \item{`"pills"`}{Selected tabs use the background fill color}
#' \item{`"hidden"`}{Hides the selectable tabs. Use `type = "hidden"` in
#' conjunction with [tabPanelBody()] and [updateTabsetPanel()] to control the
#' active tab via other input controls. (See example below)}
#' }
#' @param position This argument is deprecated; it has been discontinued in
#' Bootstrap 3.
#' @return A tabset that can be passed to [mainPanel()]
@@ -529,16 +712,46 @@ tabPanel <- function(title, ..., value = title, icon = NULL) {
#' tabPanel("Table", tableOutput("table"))
#' )
#' )
#'
#' ui <- fluidPage(
#' sidebarLayout(
#' sidebarPanel(
#' radioButtons("controller", "Controller", 1:3, 1)
#' ),
#' mainPanel(
#' tabsetPanel(
#' id = "hidden_tabs",
#' # Hide the tab values.
#' # Can only switch tabs by using `updateTabsetPanel()`
#' type = "hidden",
#' tabPanelBody("panel1", "Panel 1 content"),
#' tabPanelBody("panel2", "Panel 2 content"),
#' tabPanelBody("panel3", "Panel 3 content")
#' )
#' )
#' )
#' )
#'
#' server <- function(input, output, session) {
#' observeEvent(input$controller, {
#' updateTabsetPanel(session, "hidden_tabs", selected = paste0("panel", input$controller))
#' })
#' }
#'
#' if (interactive()) {
#' shinyApp(ui, server)
#' }
#' @export
tabsetPanel <- function(...,
id = NULL,
selected = NULL,
type = c("tabs", "pills"),
position = NULL) {
if (!is.null(position)) {
shinyDeprecated(msg = paste("tabsetPanel: argument 'position' is deprecated;",
"it has been discontinued in Bootstrap 3."),
version = "0.10.2.2")
type = c("tabs", "pills", "hidden"),
position = deprecated()) {
if (lifecycle::is_present(position)) {
shinyDeprecated(
"0.10.2.2", "bootstrapPage(position =)",
details = "The 'position' argument is no longer used with the latest version of Bootstrap."
)
}
if (!is.null(id))
@@ -669,7 +882,7 @@ findAndMarkSelectedTab <- function(tabs, selected, foundSelected) {
foundSelected <<- TRUE
div <- markTabAsSelected(div)
} else {
tabValue <- div$attribs$`data-value` %OR% div$attribs$title
tabValue <- div$attribs$`data-value` %||% div$attribs$title
if (identical(selected, tabValue)) {
foundSelected <<- TRUE
div <- markTabAsSelected(div)
@@ -796,15 +1009,15 @@ buildTabItem <- function(index, tabsetId, foundSelected, tabs = NULL,
#' Render a reactive output variable as text within an application page.
#' `textOutput()` is usually paired with [renderText()] and puts regular text
#' in `<div>` or `<span>`; `verbatimTextOutput()` is usually paired with
#' [renderPrint()] and provudes fixed-width text in a `<pre>`.
#' [renderPrint()] and provides fixed-width text in a `<pre>`.
#'
#' In both funtions, text is HTML-escaped prior to rendering.
#' In both functions, text is HTML-escaped prior to rendering.
#'
#' @param outputId output variable to read the value from
#' @param container a function to generate an HTML element to contain the text
#' @param inline use an inline (`span()`) or block container (`div()`)
#' for the output
#' @return A output element for use in UI.
#' @return An output element for use in UI.
#' @examples
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
@@ -827,14 +1040,14 @@ textOutput <- function(outputId, container = if (inline) span else div, inline =
#' @param placeholder if the output is empty or `NULL`, should an empty
#' rectangle be displayed to serve as a placeholder? (does not affect
#' behavior when the the output in nonempty)
#' behavior when the output is nonempty)
#' @export
#' @rdname textOutput
verbatimTextOutput <- function(outputId, placeholder = FALSE) {
pre(id = outputId,
class = paste(c("shiny-text-output", if (!placeholder) "noplaceholder"),
collapse = " ")
)
class = "shiny-text-output",
class = if (!placeholder) "noplaceholder"
)
}
@@ -842,44 +1055,13 @@ verbatimTextOutput <- function(outputId, placeholder = FALSE) {
#' @rdname plotOutput
#' @export
imageOutput <- function(outputId, width = "100%", height="400px",
click = NULL, dblclick = NULL,
hover = NULL, hoverDelay = NULL, hoverDelayType = NULL,
brush = NULL,
clickId = NULL, hoverId = NULL,
click = NULL, dblclick = NULL, hover = NULL, brush = NULL,
inline = FALSE) {
if (!is.null(clickId)) {
shinyDeprecated(
msg = paste("The 'clickId' argument is deprecated. ",
"Please use 'click' instead. ",
"See ?imageOutput or ?plotOutput for more information."),
version = "0.11.1"
)
click <- clickId
}
if (!is.null(hoverId)) {
shinyDeprecated(
msg = paste("The 'hoverId' argument is deprecated. ",
"Please use 'hover' instead. ",
"See ?imageOutput or ?plotOutput for more information."),
version = "0.11.1"
)
hover <- hoverId
}
if (!is.null(hoverDelay) || !is.null(hoverDelayType)) {
shinyDeprecated(
msg = paste("The 'hoverDelay'and 'hoverDelayType' arguments are deprecated. ",
"Please use 'hoverOpts' instead. ",
"See ?imageOutput or ?plotOutput for more information."),
version = "0.11.1"
)
hover <- hoverOpts(id = hover, delay = hoverDelay, delayType = hoverDelayType)
}
style <- if (!inline) {
paste("width:", validateCssUnit(width), ";", "height:", validateCssUnit(height))
# Using `css()` here instead of paste/sprintf so that NULL values will
# result in the property being dropped altogether
css(width = validateCssUnit(width), height = validateCssUnit(height))
}
@@ -984,14 +1166,6 @@ imageOutput <- function(outputId, width = "100%", height="400px",
#' named list with `x` and `y` elements indicating the mouse
#' position. To control the hover time or hover delay type, you must use
#' [hoverOpts()].
#' @param clickId Deprecated; use `click` instead. Also see the
#' [clickOpts()] function.
#' @param hoverId Deprecated; use `hover` instead. Also see the
#' [hoverOpts()] function.
#' @param hoverDelay Deprecated; use `hover` instead. Also see the
#' [hoverOpts()] function.
#' @param hoverDelayType Deprecated; use `hover` instead. Also see the
#' [hoverOpts()] function.
#' @param brush Similar to the `click` argument, this can be `NULL`
#' (the default), a string, or an object created by the
#' [brushOpts()] function. If you use a value like
@@ -1175,16 +1349,12 @@ imageOutput <- function(outputId, width = "100%", height="400px",
#' }
#' @export
plotOutput <- function(outputId, width = "100%", height="400px",
click = NULL, dblclick = NULL,
hover = NULL, hoverDelay = NULL, hoverDelayType = NULL,
brush = NULL,
clickId = NULL, hoverId = NULL,
click = NULL, dblclick = NULL, hover = NULL, brush = NULL,
inline = FALSE) {
# Result is the same as imageOutput, except for HTML class
res <- imageOutput(outputId, width, height, click, dblclick,
hover, hoverDelay, hoverDelayType, brush,
clickId, hoverId, inline)
hover, brush, inline)
res$attribs$class <- "shiny-plot-output"
res
@@ -1305,22 +1475,30 @@ uiOutput <- htmlOutput
#' is assigned to.
#' @param label The label that should appear on the button.
#' @param class Additional CSS classes to apply to the tag, if any.
#' @param icon An [icon()] to appear on the button. Default is `icon("download")`.
#' @param ... Other arguments to pass to the container tag function.
#'
#' @examples
#' \dontrun{
#' # In server.R:
#' output$downloadData <- downloadHandler(
#' filename = function() {
#' paste('data-', Sys.Date(), '.csv', sep='')
#' },
#' content = function(con) {
#' write.csv(data, con)
#' }
#' ui <- fluidPage(
#' downloadButton("downloadData", "Download")
#' )
#'
#' # In ui.R:
#' downloadLink('downloadData', 'Download')
#' server <- function(input, output) {
#' # Our dataset
#' data <- mtcars
#'
#' output$downloadData <- downloadHandler(
#' filename = function() {
#' paste("data-", Sys.Date(), ".csv", sep="")
#' },
#' content = function(file) {
#' write.csv(data, file)
#' }
#' )
#' }
#'
#' shinyApp(ui, server)
#' }
#'
#' @aliases downloadLink
@@ -1328,13 +1506,15 @@ uiOutput <- htmlOutput
#' @export
downloadButton <- function(outputId,
label="Download",
class=NULL, ...) {
class=NULL,
...,
icon = shiny::icon("download")) {
aTag <- tags$a(id=outputId,
class=paste('btn btn-default shiny-download-link', class),
href='',
target='_blank',
download=NA,
icon("download"),
validateIcon(icon),
label, ...)
}
@@ -1358,21 +1538,22 @@ downloadLink <- function(outputId, label="Download", class=NULL, ...) {
#'
#' @param name Name of icon. Icons are drawn from the
#' [Font Awesome Free](https://fontawesome.com/) (currently icons from
#' the v5.3.1 set are supported with the v4 naming convention) and
#' [Glyphicons](http://getbootstrap.com/components/#glyphicons)
#' the v5.13.0 set are supported with the v4 naming convention) and
#' [Glyphicons](https://getbootstrap.com/components/#glyphicons)
#' libraries. Note that the "fa-" and "glyphicon-" prefixes should not be used
#' in icon names (i.e. the "fa-calendar" icon should be referred to as
#' "calendar")
#' @param class Additional classes to customize the style of the icon (see the
#' [usage examples](http://fontawesome.io/examples/) for details on
#' [usage examples](https://fontawesome.com/how-to-use) for details on
#' supported styles).
#' @param lib Icon library to use ("font-awesome" or "glyphicon")
#' @param ... Arguments passed to the `<i>` tag of [htmltools::tags]
#'
#' @return An icon element
#'
#' @seealso For lists of available icons, see
#' [http://fontawesome.io/icons/](http://fontawesome.io/icons/) and
#' [http://getbootstrap.com/components/#glyphicons](http://getbootstrap.com/components/#glyphicons).
#' [https://fontawesome.com/icons](https://fontawesome.com/icons) and
#' [https://getbootstrap.com/components/#glyphicons](https://getbootstrap.com/components/#glyphicons).
#'
#'
#' @examples
@@ -1385,7 +1566,7 @@ downloadLink <- function(outputId, label="Download", class=NULL, ...) {
#' tabPanel("Table", icon = icon("table"))
#' )
#' @export
icon <- function(name, class = NULL, lib = "font-awesome") {
icon <- function(name, class = NULL, lib = "font-awesome", ...) {
prefixes <- list(
"font-awesome" = "fa",
"glyphicon" = "glyphicon"
@@ -1411,12 +1592,12 @@ icon <- function(name, class = NULL, lib = "font-awesome") {
if (!is.null(class))
iconClass <- paste(iconClass, class)
iconTag <- tags$i(class = iconClass)
iconTag <- tags$i(class = iconClass, role = "presentation", `aria-label` = paste(name, "icon"), ...)
# font-awesome needs an additional dependency (glyphicon is in bootstrap)
if (lib == "font-awesome") {
htmlDependencies(iconTag) <- htmlDependency(
"font-awesome", "5.3.1", "www/shared/fontawesome", package = "shiny",
"font-awesome", "5.13.0", "www/shared/fontawesome", package = "shiny",
stylesheet = c(
"css/all.min.css",
"css/v4-shims.min.css"

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

354
R/devmode.R Normal file
View File

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

View File

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

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)

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

View File

@@ -91,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
)
@@ -102,7 +103,7 @@ 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",

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

@@ -79,17 +79,22 @@ 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)
{
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")
shinyDeprecated(
"0.10.2.2", "sliderInput(format =)",
details = "Use `sep`, `pre`, and `post` instead."
)
}
if (!missing(locale)) {
shinyDeprecated(msg = "The `locale` argument to sliderInput is deprecated. Use `sep`, `pre`, and `post` instead.",
version = "0.10.2.2")
shinyDeprecated(
"0.10.2.2", "sliderInput(locale =)",
details = "Use `sep`, `pre`, and `post` instead."
)
}
validate_slider_value(min, max, value, "sliderInput")
dataType <- getSliderType(min, max, value)
if (is.null(timeFormat)) {
@@ -144,6 +149,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 +181,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 +211,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 +276,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 +302,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
)
}

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

View File

@@ -9,8 +9,6 @@ wait_for_it <- function() {
# Block until the promise is resolved/rejected. If resolved, return the value.
# If rejected, throw (yes throw, not return) the error.
#' @importFrom promises %...!%
#' @importFrom promises %...>%
extract <- function(promise) {
promise_value <- NULL
error <- NULL
@@ -31,7 +29,6 @@ extract <- function(promise) {
#' @noRd
#' @export
`$.mockclientdata` <- function(x, name) {
if (name == "allowDataUriScheme") { return(TRUE) }
if (name == "pixelratio") { return(1) }
if (name == "url_protocol") { return("http:") }
if (name == "url_hostname") { return("mocksession") }
@@ -69,45 +66,198 @@ extract <- function(promise) {
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 that simulates the `session` parameter
#' provided to Shiny server functions or modules.
#' @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,
class = FALSE,
lock_objects = FALSE,
public = list(
#' @field env The environment associated with the session.
env = NULL,
#' @field singletons Hardcoded as empty. Needed for rendering HTML (i.e. renderUI)
#' @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
#' @field clientData Mock client data that always returns a size for plots.
clientData = structure(list(), class="mockclientdata"),
#' @description No-op
#' @param logEntry Not used
reactlog = function(logEntry){},
#' @description No-op
incrementBusyCount = function(){},
#' @field output The shinyoutputs associated with the session
#' @field output The shinyoutputs associated with the session.
output = NULL,
#' @field input The reactive inputs associated with the session
#' @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
#' @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
#' @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 <- Stack$new()
@@ -120,6 +270,19 @@ MockShinySession <- R6Class(
# 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
@@ -156,16 +319,24 @@ MockShinySession <- R6Class(
},
#' @description Returns `FALSE` if the session has not yet been closed
isEnded = function(){ private$closed },
isEnded = function(){ private$was_closed },
#' @description Returns `FALSE` if the session has not yet been closed
isClosed = function(){ private$closed },
isClosed = function(){ private$was_closed },
#' @description Closes the session
close = function(){ private$closed <- TRUE },
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 ato be invoked.
# 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.
@@ -182,14 +353,17 @@ MockShinySession <- R6Class(
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.
#' @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
#' s <- MockShinySession$new()
#' s$setInputs(x=1, y=2)
#' \dontrun{
#' session$setInputs(x=1, y=2)
#' }
setInputs = function(...) {
vals <- list(...)
vals <- rlang::dots_list(..., .homonyms = "error")
mapply(names(vals), vals, FUN = function(name, value) {
private$.input$set(name, value)
})
@@ -197,8 +371,10 @@ MockShinySession <- R6Class(
},
#' @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
#' @param callback The function to schedule.
.scheduleTask = function(millis, callback) {
id <- private$timer$schedule(millis, callback)
@@ -237,15 +413,17 @@ MockShinySession <- R6Class(
},
#' @description An internal method which shouldn't be used by others.
#' @return Elapsed time in milliseconds.
.now = function() {
# Returns elapsed time in milliseconds
private$timer$getElapsed()
},
#' @description An internal method which shouldn't be used by others.
#' @param name The name of the output
#' @param func The render definition
#' @param label Not used
#' 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)
@@ -262,7 +440,7 @@ MockShinySession <- R6Class(
# We could just stash the promise, but we get an "unhandled promise error". This bypasses
prom <- NULL
tryCatch({
v <- func(self, name)
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) })
@@ -285,8 +463,11 @@ MockShinySession <- R6Class(
private$outs[[name]] <- list(obs = obs, func = func, promise = NULL)
},
#' @description An internal method which shouldn't be used by others.
#' @param name The name of the output
#' @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]])) {
@@ -307,120 +488,225 @@ MockShinySession <- R6Class(
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 No-op
#' @param name Not used
#' @param data Not used
#' @param filterFunc Not used
registerDataObj = function(name, data, filterFunc) {},
#' @description No-op
#' @param value Not used
allowReconnect = function(value) {},
#' @description No-op
reload = function() {},
#' @description No-op
#' @param brushId Not used
resetBrush = function(brushId) {
warning("session$brush isn't meaningfully mocked on the MockShinySession")
},
#' @description No-op
#' @param type Not used
#' @param message Not used
sendCustomMessage = function(type, message) {},
#' @description No-op
#' @param type Not used
#' @param message Not used
sendBinaryMessage = function(type, message) {},
#' @description No-op
#' @param inputId Not used
#' @param message Not used
sendInputMessage = function(inputId, message) {},
#' @description No-op
#' @param names Not used
setBookmarkExclude = function(names) {
warning("Bookmarking isn't meaningfully mocked in MockShinySession")
},
#' @description No-op
getBookmarkExclude = function() {
warning("Bookmarking isn't meaningfully mocked in MockShinySession")
},
#' @description No-op
#' @param fun Not used
onBookmark = function(fun) {},
#' @description No-op
#' @param fun Not used
onBookmarked = function(fun) {},
#' @description No-op
doBookmark = function() {
warning("Bookmarking isn't meaningfully mocked in MockShinySession")
},
#' @description No-op
#' @param fun Not used
onRestore = function(fun) {},
#' @description No-op
#' @param fun Not used
onRestored = function(fun) {},
#' @description No-op
exportTestValues = function() {},
#' @description No-op
#' @param input Not used
#' @param output Not used
#' @param export Not used
#' @param format Not used
getTestSnapshotUrl = function(input=TRUE, output=TRUE, export=TRUE, format="json") {},
#' @description Returns the given id prefixed by `mock-session-`.
#' @param id The id to modify.
#' @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) {
paste0("mock-session-", id) # TODO: does this need to be more complex/intelligent?
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))
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,
closed = FALSE,
# @field was_closed Set to `TRUE` once the session is closed.
was_closed = FALSE,
# @field outs List of namespaced output names.
outs = list(),
returnedVal = NULL,
# @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(
# If assigning to `returned`, proactively flush
#' @field returned The value returned from the module
returned = function(value){
if(missing(value)){
return(private$returnedVal)
}
# When you assign to returned, that implies that you just ran
# the module. So we should proactively flush. We have to do this
# here since flush is private.
private$returnedVal <- value
private$flush()
},
#' @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)){
@@ -431,4 +717,3 @@ MockShinySession <- R6Class(
}
)
)

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,32 +31,56 @@ createSessionProxy <- function(parentSession, ...) {
# but not `session$userData <- TRUE`) from within a module
# without any hacks (see PR #1732)
if (identical(x[[name]], value)) return(x)
# Special case for $options (issue #3112)
if (name == "options") {
session <- find_ancestor_session(x)
session[[name]] <- value
return(x)
}
stop("Attempted to assign value on session proxy.")
}
`[[<-.session_proxy` <- `$<-.session_proxy`
# Given a session_proxy, search `parent` recursively to find the real
# ShinySession object. If given a ShinySession, simply return it.
find_ancestor_session <- function(x, depth = 20) {
if (depth < 0) {
stop("ShinySession not found")
}
if (inherits(x, "ShinySession")) {
return(x)
}
if (inherits(x, "session_proxy")) {
return(find_ancestor_session(.subset2(x, "parent"), depth-1))
}
stop("ShinySession not found")
}
#' Shiny modules
#'
#' Shiny's module feature lets you break complicated UI and server logic into
#' smaller, self-contained pieces. Compared to large monolithic Shiny apps,
#' modules are easier to reuse and easier to reason about. See the article at
#' <http://shiny.rstudio.com/articles/modules.html> to learn more.
#' <https://shiny.rstudio.com/articles/modules.html> to learn more.
#'
#' Starting in Shiny 1.5.0, we recommend using `moduleFunction` instead of
#' `callModule`, because syntax is a little easier to understand.
#' 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 ... For `callModule`, additional parameters to pass to module server
#' 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 <http://shiny.rstudio.com/articles/modules.html>
#' @seealso <https://shiny.rstudio.com/articles/modules.html>
#'
#' @examples
#' # Define the UI for a module
@@ -70,16 +94,19 @@ createSessionProxy <- function(parentSession, ...) {
#'
#' # 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
#' })
#' 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
@@ -91,7 +118,9 @@ createSessionProxy <- function(parentSession, ...) {
#' counterServer("counter1")
#' counterServer("counter2")
#' }
#' shinyApp(ui, server)
#' if (interactive()) {
#' shinyApp(ui, server)
#' }
#'
#'
#'
@@ -99,16 +128,19 @@ createSessionProxy <- function(parentSession, ...) {
#' # 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
#' })
#' moduleServer(
#' id,
#' function(input, output, session) {
#' count <- reactiveVal(0)
#' observeEvent(input$button, {
#' count(count() + 1)
#' })
#' output$out <- renderText({
#' paste0(prefix, count())
#' })
#' count
#' }
#' )
#' }
#'
#' ui <- fluidPage(
@@ -117,18 +149,42 @@ createSessionProxy <- function(parentSession, ...) {
#' server <- function(input, output, session) {
#' counterServer2("counter", "The current count is: ")
#' }
#' shinyApp(ui, server)
#' if (interactive()) {
#' shinyApp(ui, server)
#' }
#'
#' @export
moduleServer <- function(id, module, session = getDefaultReactiveDomain()) {
callModule(module, id, session = session)
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)
}
}
#' @rdname moduleServer
#' 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
#' UI function
#' @param ... Additional parameters to pass to module server 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
#' @export
callModule <- function(module, id, ..., session = getDefaultReactiveDomain()) {
if (!inherits(session, "ShinySession") && !inherits(session, "session_proxy")) {
if (!inherits(session, c("ShinySession", "session_proxy", "MockShinySession"))) {
stop("session must be a ShinySession or session_proxy object.")
}
childScope <- session$makeScope(id)

View File

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

View File

@@ -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 observer()?"
))
}
.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
@@ -1737,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
@@ -1744,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()
}
@@ -2087,6 +2173,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
@@ -2159,42 +2246,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)
}
@@ -2204,34 +2287,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) {
@@ -2391,7 +2466,7 @@ debounce <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
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)

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

@@ -53,8 +53,10 @@ renderTable <- function(expr, striped = FALSE, hover = FALSE,
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.

View File

@@ -8,31 +8,55 @@ getShinyOption <- function(name, default = NULL) {
# Make sure to use named (not numeric) indexing
name <- as.character(name)
if (name %in% names(.globals$options))
.globals$options[[name]]
else
default
# Check if there's a current session
session <- getDefaultReactiveDomain()
if (!is.null(session)) {
if (name %in% names(session$options)) {
return(session$options[[name]])
} else {
return(default)
}
}
# Check if there's a current app
app_state <- getCurrentAppState()
if (!is.null(app_state)) {
if (name %in% names(app_state$options)) {
return(app_state$options[[name]])
} else {
return(default)
}
}
# If we got here, look in global options
if (name %in% names(.globals$options)) {
return(.globals$options[[name]])
} else {
return(default)
}
}
#' Get or set Shiny options
#'
#' `getShinyOption()` retrieves the value of a Shiny option. `shinyOptions()`
#' sets the value of Shiny options; it can also be used to return a list of all
#' currently-set Shiny options.
#' @description
#'
#' @section Scope:
#' There is a global option set which is available by default. When a Shiny
#' application is run with [runApp()], that option set is duplicated and the
#' new option set is available for getting or setting values. If options
#' are set from `global.R`, `app.R`, `ui.R`, or `server.R`, or if they are set
#' from inside the server function, then the options will be scoped to the
#' application. When the application exits, the new option set is discarded and
#' the global option set is restored.
#' There are two mechanisms for working with options for Shiny. One is the
#' [options()] function, which is part of base R, and the other is the
#' `shinyOptions()` function, which is in the Shiny package. The reason for
#' these two mechanisms is has to do with legacy code and scoping.
#'
#' @section Options:
#' There are a number of global options that affect Shiny's behavior. These can
#' be set globally with `options()` or locally (for a single app) with
#' `shinyOptions()`.
#' The [options()] function sets options globally, for the duration of the R
#' process. The [getOption()] function retrieves the value of an option. All
#' shiny related options of this type are prefixed with `"shiny."`.
#'
#' The `shinyOptions()` function sets the value of a shiny option, but unlike
#' `options()`, it is not always global in scope; the options may be scoped
#' globally, to an application, or to a user session in an application,
#' depending on the context. The `getShinyOption()` function retrieves a value
#' of a shiny option. Currently, the options set via `shinyOptions` are for
#' internal use only.
#'
#' @section Options with `options()`:
#'
#' \describe{
#' \item{shiny.autoreload (defaults to `FALSE`)}{If `TRUE` when a Shiny app is launched, the
@@ -65,7 +89,7 @@ getShinyOption <- function(name, default = NULL) {
#' [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.}
#' then jQuery 3.5.1 is used.}
#' \item{shiny.json.digits (defaults to `16`)}{The number of digits to use when converting
#' numbers to JSON format to send to the client web browser.}
#' \item{shiny.launch.browser (defaults to `interactive()`)}{A boolean which controls the default behavior
@@ -107,46 +131,126 @@ getShinyOption <- function(name, default = NULL) {
#' `"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)
}

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

@@ -0,0 +1,35 @@
# 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 digest digest
#' @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
#' 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

483
R/shiny.R
View File

@@ -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)
@@ -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,14 +608,14 @@ 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
@@ -651,6 +674,7 @@ 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
@@ -681,16 +705,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)) {
@@ -928,7 +962,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))
},
@@ -962,7 +1022,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)
@@ -1115,7 +1177,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) {
@@ -1236,6 +1301,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)
@@ -1301,39 +1426,97 @@ ShinySession <- R6Class(
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]]
@@ -1350,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.",
@@ -1626,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) {
@@ -1696,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') {
@@ -1797,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')))
@@ -1831,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) {
@@ -1992,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
}
@@ -2097,16 +2238,69 @@ outputOptions <- function(x, name, ...) {
}
#' Get information about the output that is currently being executed.
#' Get output information
#'
#' @return A list with information about the current output, including the
#' `name` of the output. If no output is currently being executed, this will
#' return `NULL`.
#' 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()
}
@@ -2294,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

@@ -93,8 +93,7 @@ shinyApp <- function(ui, server, onStart=NULL, options=list(),
# Store the appDir and bookmarking-related options, so that we can read them
# from within the app.
shinyOptions(appDir = getwd())
appOptions <- consumeAppOptions()
appOptions <- captureAppOptions()
structure(
list(
@@ -114,7 +113,10 @@ shinyApp <- function(ui, server, onStart=NULL, options=list(),
#' @export
shinyAppDir <- function(appDir, options=list()) {
if (!utils::file_test('-d', appDir)) {
stop("No Shiny application exists at the path \"", appDir, "\"")
rlang::abort(
paste0("No Shiny application exists at the path \"", appDir, "\""),
class = "invalidShinyAppDir"
)
}
# In case it's a relative path, convert to absolute (so we're not adversely
@@ -126,7 +128,10 @@ shinyAppDir <- function(appDir, options=list()) {
} else if (file.exists.ci(appDir, "app.R")) {
shinyAppDir_appR("app.R", appDir, options = options)
} else {
stop("App dir must contain either app.R or server.R.")
rlang::abort(
"App dir must contain either app.R or server.R.",
class = "invalidShinyAppDir"
)
}
}
@@ -227,7 +232,6 @@ 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", TRUE)) {
loadSupport(appDir, renv=sharedEnv, globalrenv=globalenv())
@@ -235,11 +239,17 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
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(
@@ -286,8 +296,9 @@ initAutoReloadMonitor <- function(dir) {
lastValue <- NULL
observeLabel <- paste0("File Auto-Reload - '", basename(dir), "'")
obs <- observe(label = observeLabel, {
files <- sort(list.files(dir, pattern = filePattern, recursive = TRUE,
ignore.case = TRUE))
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
}
@@ -325,37 +336,60 @@ initAutoReloadMonitor <- function(dir) {
#' @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")
disabled <- list.files(helpersDir, pattern="^_disable_autoload\\.r$", recursive=FALSE, ignore.case=TRUE)
if (length(disabled) > 0){
message("R/_disable_autoload.R detected; not loading the R/ directory automatically")
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)
if (length(helpers) > 0){
message("Automatically loading ", length(helpers), " .R file",
ifelse(length(helpers) != 1, "s", ""),
" found in the R/ directory.\nSee https://rstd.io/shiny-autoload for more info.")
}
lapply(helpers, sourceUTF8, envir=renv)
withr::with_dir(appDir, {
lapply(helpers, sourceUTF8, envir=renv)
})
invisible(renv)
}
@@ -367,27 +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.
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 {
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)
}
@@ -425,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", TRUE)) {
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
@@ -456,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"
)
@@ -506,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
@@ -527,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
@@ -547,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", "3.5.1",
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,7 +748,7 @@ 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)
)
}
@@ -577,7 +773,7 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
#' 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 (<https://datatables.net/extensions/>).
#' @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
@@ -588,14 +784,14 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
#' call to [dataTableOutput()] when `renderDataTable` is used
#' in an interactive R Markdown document.
#'
#' @references <http://datatables.net>
#' @references <https://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).
#' <https://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,157 +0,0 @@
#' Integration testing for Shiny modules or server functions
#'
#' Offer a way to test the reactive interactions in Shiny --- either in Shiny
#' modules or in the server portion of a Shiny application. For more
#' information, visit [the Shiny Dev Center article on integration
#' testing](https://shiny.rstudio.com/articles/integration-testing.html).
#' @param module The module to test
#' @param expr Test code containing expectations. The test expression will run
#' in the module's environment, meaning that the module's parameters (e.g.
#' `input`, `output`, and `session`) will be available along with any other
#' values created inside of the module.
#' @param ... Additional arguments to pass to the module function. These
#' arguments are processed with [rlang::list2()] and so are
#' _[dynamic][rlang::dyn-dots]_.
#' @include mock-session.R
#' @rdname testModule
#' @examples
#' module <- function(input, output, session, multiplier = 2, prefix = "I am ") {
#' myreactive <- reactive({
#' input$x * multiplier
#' })
#' output$txt <- renderText({
#' paste0(prefix, myreactive())
#' })
#' }
#'
#' # Basic Usage
#' # -----------
#' testModule(module, {
#' 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.
#' }, multiplier = 2)
#'
#' # Advanced Usage
#' # --------------
#' multiplier_arg_name = "multiplier"
#' more_args <- list(prefix = "I am ")
#' testModule(module, {
#' session$setInputs(x = 1)
#' stopifnot(myreactive() == 2)
#' stopifnot(output$txt == "I am 2")
#' # !!/:= and !!! from rlang are used below to splice computed arguments
#' # into the testModule() argument list.
#' }, !!multiplier_arg_name := 2, !!!more_args)
#' @export
testModule <- function(module, expr, ...) {
expr <- substitute(expr)
.testModule(module, expr, ...)
}
#' @noRd
#' @importFrom withr with_options
.testModule <- function(module, expr, ...) {
# Capture the environment from the module
# Inserts `session$env <- environment()` at the top of the function
body(module) <- rlang::expr({
session$env <- environment()
!!!body(module)
})
# Create a mock session
session <- MockShinySession$new()
# Parse the additional arguments
args <- rlang::list2(..., input = session$input, output = session$output, session = session)
# Initialize the module
isolate(
withReactiveDomain(
session,
withr::with_options(list(`shiny.allowoutputreads`=TRUE), {
# Remember that invoking this module implicitly assigns to `session$env`
# Also, assigning to `$returned` will cause a flush to happen automatically.
session$returned <- do.call(module, args)
})
)
)
# Run the test expression in a reactive context and in the module's environment.
# We don't need to flush before entering the loop because the first expr that we execute is `{`.
# So we'll already flush before we get to the good stuff.
isolate({
withReactiveDomain(
session,
withr::with_options(list(`shiny.allowoutputreads`=TRUE), {
eval(expr, new.env(parent=session$env))
})
)
})
if (!session$isClosed()){
session$close()
}
}
#' Test an app's server-side logic
#' @param appDir The directory root of the Shiny application. If `NULL`, this function
#' will work up the directory hierarchy --- starting with the current directory ---
#' looking for a directory that contains an `app.R` or `server.R` file.
#' @rdname testModule
#' @export
testServer <- function(expr, appDir=NULL) {
if (is.null(appDir)){
appDir <- findApp()
}
app <- shinyAppDir(appDir)
message("Testing application found in: ", appDir)
server <- app$serverFuncSource()
origwd <- getwd()
setwd(appDir)
on.exit({ setwd(origwd) }, add=TRUE)
# Add `session` argument if not present
fn_formals <- formals(server)
if (! "session" %in% names(fn_formals)) {
fn_formals$session <- bquote()
formals(server) <- fn_formals
}
# Now test the server as we would a module
.testModule(server, expr=substitute(expr))
}
findApp <- function(startDir="."){
dir <- normalizePath(startDir)
# The loop will either return or stop() itself.
while (TRUE){
if(file.exists.ci(file.path(dir, "app.R")) || file.exists.ci(file.path(dir, "server.R"))){
return(dir)
}
# Move up a directory
origDir <- dir
dir <- dirname(dir)
# Testing for "root" path can be tricky. OSs differ and on Windows, network shares
# might have a \\ prefix. Easier to just see if we got stuck and abort.
if (dir == origDir){
# We can go no further.
stop("No shiny app was found in ", startDir, " or any of its parent directories")
}
}
}

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"))
}

222
R/test.R
View File

@@ -1,11 +1,48 @@
#' Check to see if the given text is a shinytest
#' Scans for the magic string of `app <- ShinyDriver$new(` as an indicator that this is a shinytest.
#' Brought in from shinytest to avoid having to export this function.
#' 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
isShinyTest <- function(text){
lines <- grepl("app\\s*<-\\s*ShinyDriver\\$new\\(", text, perl=TRUE)
any(lines)
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
@@ -18,90 +55,135 @@ isShinyTest <- function(text){
#' @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. In
#' order to support that model, `testApp` first checks to see if the `.R`
#' files in the `tests/` directory are all shinytests; if so, just calls out
#' to [shinytest::testApp()].
#' 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){
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)){
if (!dirExists(testsDir)) {
stop("No tests directory found: ", testsDir)
}
runners <- list.files(testsDir, pattern="\\.r$", ignore.case = TRUE)
if (length(runners) == 0){
if (length(runners) == 0) {
message("No test runners found in ", testsDir)
return(structure(list(result=NA, files=list()), class="shinytestrun"))
return(result_row(character(0), logical(0), list()))
}
if (!is.null(filter)){
if (!is.null(filter)) {
runners <- runners[grepl(filter, runners)]
}
if (length(runners) == 0){
if (length(runners) == 0) {
stop("No test runners matched the given filter: '", filter, "'")
}
# Inspect each runner to see if it appears to be a shinytest
isST <- vapply(runners, function(r){
text <- readLines(file.path(testsDir, r), warn = FALSE)
isShinyTest(text)
}, logical(1))
if (all(isST)){
# just call out to shinytest
# We don't need to message/warn here since shinytest already does it.
if (!requireNamespace("shinytest", quietly=TRUE) ){
stop("It appears that the .R files in ", testsDir,
" are all shinytests, but shinytest is not installed.")
}
if (!getOption("shiny.autoload.r", TRUE)) {
warning("You've disabled `shiny.autoload.r` via an option but this is not passed through to shinytest. Consider using a _disable_autoload.R file as described at https://rstd.io/shiny-autoload")
}
sares <- shinytest::testApp(appDir)
res <- list()
lapply(sares$results, function(r){
e <- NA_character_
if (!r$pass){
e <- simpleError("Unknown shinytest error")
}
res[[r$name]] <<- e
})
return(structure(list(result=all(is.na(res)), files=res), class="shinytestrun"))
# 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."
)
}
testenv <- new.env(parent=globalenv())
renv <- new.env(parent=testenv)
if (getOption("shiny.autoload.r", TRUE)) {
loadSupport(appDir, renv=renv, globalrenv=testenv)
} else if (file.exists.ci(file.path(appDir, "server.R"))){
# then check for global.R to load
if (file.exists(file.path.ci(appDir, "global.R"))){
sourceUTF8(file.path.ci(appDir, "global.R"))
}
}
oldwd <- getwd()
on.exit({
setwd(oldwd)
}, add=TRUE)
setwd(testsDir)
renv <- new.env(parent = envir)
# Otherwise source all the runners -- each in their own environment.
fileResults <- list()
lapply(runners, function(r){
env <- new.env(parent=renv)
tryCatch({sourceUTF8(r, envir=env); fileResults[[r]] <<- NA_character_}, error=function(e){
fileResults[[r]] <<- e
})
})
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))
}))
return(structure(list(result=all(is.na(fileResults)), files=fileResults), class="shinytestrun"))
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,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
}

284
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).
#'
@@ -1484,7 +1503,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)
@@ -1568,15 +1587,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,
@@ -1599,18 +1622,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
}
@@ -1670,19 +1692,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)
}
})
},
@@ -1703,24 +1729,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)
@@ -1764,7 +1779,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]]
}
@@ -1812,3 +1830,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
}
})

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,54 +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:
# Bust library cache every time the description file changes
# as appveyor cache can not be busted manually
# This helps get around errors such as "can't update curl because it's already loaded"
# when trying to update the existing cache
# PR: https://github.com/rstudio/shiny/pull/2722
- C:\RLibrary -> DESCRIPTION
# Adapt as necessary starting from here
build_script:
- 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,18 @@
context("exampleModuleServer")
# 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,20 @@
context("app")
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,7 @@
# Test the lexical_sort function from R/example.R
context("sort")
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

@@ -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 */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates["en-CA"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:0,format:"yyyy-mm-dd"},a.fn.datepicker.deprecated("This filename doesn't follow the convention, use bootstrap-datepicker.en-CA.js instead.")}(jQuery);

View File

@@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates["ar-tn"]={days:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت","الأحد"],daysShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت","أحد"],daysMin:["ح","ن","ث","ع","خ","ج","س","ح"],months:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthsShort:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],today:"هذا اليوم",rtl:!0}}(jQuery);

View File

@@ -1 +1 @@
!function(a){a.fn.datepicker.dates.az={days:["Bazar","Bazar ertəsi","Çərşənbə axşamı","Çərşənbə","Cümə axşamı","Cümə","Şənbə"],daysShort:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],daysMin:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],months:["Yanvar","Fevral","Mart","Aprel","May","İyun","İyul","Avqust","Sentyabr","Oktyabr","Noyabr","Dekabr"],monthsShort:["Yan","Fev","Mar","Apr","May","İyun","İyul","Avq","Sen","Okt","Noy","Dek"],today:"Bu gün",weekStart:1}}(jQuery);
!function(a){a.fn.datepicker.dates.az={days:["Bazar","Bazar ertəsi","Çərşənbə axşamı","Çərşənbə","Cümə axşamı","Cümə","Şənbə"],daysShort:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],daysMin:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],months:["Yanvar","Fevral","Mart","Aprel","May","İyun","İyul","Avqust","Sentyabr","Oktyabr","Noyabr","Dekabr"],monthsShort:["Yan","Fev","Mar","Apr","May","İyun","İyul","Avq","Sen","Okt","Noy","Dek"],today:"Bu gün",weekStart:1,clear:"Təmizlə",monthsTitle:"Aylar"}}(jQuery);

View File

@@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates.bm={days:["Kari","Ntɛnɛn","Tarata","Araba","Alamisa","Juma","Sibiri"],daysShort:["Kar","Ntɛ","Tar","Ara","Ala","Jum","Sib"],daysMin:["Ka","Nt","Ta","Ar","Al","Ju","Si"],months:["Zanwuyekalo","Fewuruyekalo","Marisikalo","Awirilikalo","Mɛkalo","Zuwɛnkalo","Zuluyekalo","Utikalo","Sɛtanburukalo","ɔkutɔburukalo","Nowanburukalo","Desanburukalo"],monthsShort:["Zan","Few","Mar","Awi","Mɛ","Zuw","Zul","Uti","Sɛt","ɔku","Now","Des"],today:"Bi",monthsTitle:"Kalo",clear:"Ka jɔsi",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);

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