Compare commits

...

748 Commits

Author SHA1 Message Date
Winston Chang
431d2a0127 Grunt 2018-10-16 14:20:24 -05:00
Winston Chang
db446778cf Copy over unminified version of bootstrap-datepicker.js 2018-10-16 14:20:14 -05:00
Winston Chang
b89785429c ls 2018-10-16 14:17:58 -05:00
Winston Chang
0011374864 Re-document 2018-10-16 13:59:36 -05:00
Winston Chang
9b0c63c9e6 Update locales for dateInput 2018-10-16 13:58:31 -05:00
Winston Chang
1cdbe79422 Update NEWS 2018-10-16 13:44:16 -05:00
Winston Chang
a497c3e63b Update version in datePickerDependency 2018-10-16 13:39:44 -05:00
Winston Chang
5c71e1ffdf Update bootstrap datepicker 2018-10-16 13:38:04 -05:00
Winston Chang
d3e2d57047 Modify updateBootstrapDatepicker script 2018-10-16 13:37:41 -05:00
Alan Dipert
fcd09e2bae Simplify DnD for fileInputs, fix #2142 (Firefox 57+)
- Simplified dragHover "plugin" by counting children instead of storing them.
  Counting children fixes Firefox 57+ bug (to be found or filed) that causes
  text object of input element to produce drag events
- Removed multimethod since it's no longer used anywhere
- Firefox 57+ appears not to trigger a change event when the `files` field is modified,
  which prevented uploads from occuring. This commit triggers a change event manually
  and doesn't impact the functioning of other browsers.
2018-10-08 21:24:18 -07:00
Joe Cheng
b25cb0f2d5 Merge pull request #2200 from rstudio/joe/bugfix/brush-webkit
Fix brushes not being properly cleared
2018-09-27 14:23:27 -07:00
Barret Schloerke
0704aec01b Follow js event namespacing conventions and only possibly init brush once (#2202)
* underscore the shiny_image_interaction namespace

* namespace dragstart

* use `one` instead of `on`

* compile
2018-09-27 16:24:53 -04:00
Barret Schloerke
d38b939c63 use naturalHeight and naturalWidth for default dim values. Followup comments from winston (#2201) 2018-09-27 16:16:11 -04:00
Joe Cheng
112466de1e Fix brushes not being properly cleared
Actually three separate issues addressed. Fixes #2197.

- brush.importOldBrush() was not being called anymore, due to it being
  registered as a load handler after the image was already loaded (this
  was a very recent regression, less than 24 hours old).
- Each time the brush changes, the plot is redrawn twice. This was
  because importing the old brush introduced floating point errors that
  led to a slightly different new brush being created.
- Sometimes the image's load event wasn't firing at all. This is due to
  behavior in WebKit where assigning an image's src to its existing
  value is a no-op.
2018-09-26 22:57:16 -07:00
Barret Schloerke
1d0edd2ad0 Initialize brush dims for renderImage objects (#2198)
* wait for image to be loaded in browser before initializing handlers

reverts similar behavior in 3354a47e8a

* default the height and width to the image clientHeight and clientWidth

* use raw image clientWidth and clientHeight instead of container clientWidth and clientHeight

prevents being able to brush on non image areas
2018-09-26 13:29:42 -04:00
Joe Cheng
37736119be Merge pull request #2195 from rstudio/joe/bugfix/selectize-choices-data-frame
Fix custom selectize rendering
2018-09-25 14:23:29 -07:00
Joe Cheng
c5df150acb Improve robustness of optgroup construction
Instead of providing alternate defaults for optgroupField,
optgroupLabelField, and optgroupValueField, respect the
selectize instance's settings for those fields.
2018-09-25 14:13:39 -07:00
Joe Cheng
49a346334b Fix custom selectize rendering
Fixes #2192. Two problems here:

1. It's not documented but apparently we supported data frames for
   choices in updateSelectInput/updateSelectizeInput (it doesn't
   appear to work correctly for selectInput/selectizeInput though).
   This was used in 023-optgroup-server as well as by the user who
   reported #2172.
2. The example in 023-optgroup-server was also counting on the
   default value of optgroupLabelField, which (starting post-Shiny
   v1.1) was being set to a new default of "group". That now won't
   happen unless optgroupField is also blank. I'm less confident
   about the ramifications of this change. The selectize docs with
   the relevant bits are here:
  https://github.com/selectize/selectize.js/blob/master/docs/usage.md#data_searching
2018-09-25 13:23:35 -07:00
Joe Cheng
e7c4656e8f Fix selectize bug where value is set merely on query results (#2193)
This bug is new since v1.1. When results are returned from selectize's
server-side endpoint, iff no results have been selected before, then
the control should be set to either its specified initial value (the
one specified in selectInput/selectizeInput) or, if none was provided
AND the selectize control is multiple=FALSE, then select the first
entry automatically.

That's the desired behavior; the bug was that last part, "select the
first entry automatically", was happening whether results had already
been selected before or not. This was causing merely typing in the
control to cause the value to be changed.

Fixes #2191
2018-09-25 12:21:16 -07:00
Joe Cheng
85bed0582a Rebuild JS (to update version number) 2018-09-19 09:51:36 -07:00
Joe Cheng
b9e6f867c6 Bump version 2018-09-19 09:47:41 -07:00
Joe Cheng
a5b80168bd Refactor v1.2 news 2018-09-19 09:47:35 -07:00
Alan Dipert
3cea5fb2d0 Upgrade FontAwesome to 5.3.1 (#2186)
* Upgrade FontAwesome to 5.3.1

- Upgrades FontAwesome to a new major (breaking) version, but
  is backwards compatible because we include the v4-shims CSS that maps
  old names to new.
- This is a step toward full V5 adoption that doesn't require us to
  come up with a plan for deprecating V4 icon names.
- Details: https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4
- Related to #2156 and #1966

* Improvements to icon

- Clarify in docs that fontawesome V5 icons accessible with V4-style names
- Make icons browseable: icon('address-book') will now open the Viewer
  pane of RStudio IDE so that icons can be experimented with more easily.

* Update LICENSE with CC for FontAwesome .svgs

* Update NEWS
2018-09-18 13:30:14 -07:00
Joe Cheng
c89d782048 Merge pull request #2187 from rstudio/joe/bugfix/selectize-nonempty
Fix spurious duplicate values being sent by selectInput
2018-09-18 13:27:55 -07:00
Barret Schloerke
1fd4179e07 News item for #2180 (#2189)
* news item for #2180

* fix news item
2018-09-18 14:58:57 -04:00
Joe Cheng
3b62400298 Code review feedback--use an arrow function instead of aliasing this 2018-09-18 10:52:50 -07:00
Joe Cheng
ba0fe938a1 Merge pull request #2188 from rstudio/jcheng5-patch-1
Remove unneeded Remotes
2018-09-18 10:33:47 -07:00
Joe Cheng
d4560171a8 Remove unneeded Remotes 2018-09-18 09:59:40 -07:00
Barret Schloerke
f5a23826c8 add domain to reactlog for context exit (#2180)
* add domain to reactlog for context exit

* use if statement vs %OR% when using envs

* Simplify graphExitContext domain argument
2018-09-18 12:23:01 -04:00
Joe Cheng
ef63679ff0 Update NEWS 2018-09-17 16:13:32 -07:00
Joe Cheng
ef7e1c385a Fix spurious duplicate values being sent by selectInput 2018-09-17 16:12:15 -07:00
Barret Schloerke
3a0a6cdbbb Add css and image locations to plot click and brush events (#2183) 2018-09-17 15:25:34 -04:00
Joe Cheng
340df3e956 Merge pull request #2174 from AliciaSchep/master
Add informative errors when xvar or yvar not in data for brushedPoints
2018-09-17 11:33:16 -07:00
AliciaSchep
eeb264da8e add var name to error msg 2018-09-17 09:45:40 -07:00
AliciaSchep
00f08b8ec6 revert two previous commits 2018-09-17 09:41:04 -07:00
AliciaSchep
67ae2a39ba update documentation to reflect new options for xvar and yvar 2018-09-16 23:07:08 -07:00
AliciaSchep
72dda25835 evaluate xvar and yvar for nearPoints and brushedPoints 2018-09-16 22:45:53 -07:00
Alan Dipert
8c9ce1994a Merge pull request #2028 from rstudio/joe/misc/selectize-upgrade
Upgrade Selectize to 0.12.4
2018-09-14 14:42:28 -07:00
Alan Dipert
606b05fdaf Merge remote-tracking branch 'origin' into joe/misc/selectize-upgrade 2018-09-14 14:40:54 -07:00
Alan Dipert
420ba9549f Merge pull request #2047 from rstudio/joe/bugfix/post-message
Fix #2033: Rstudio Viewer window not closed on shiny::stopApp()
2018-09-14 14:26:58 -07:00
Alan Dipert
51fbb5cfac Update NEWS 2018-09-14 14:25:43 -07:00
Alan Dipert
ca2c2b60f2 Grunt 2018-09-14 14:25:09 -07:00
Alan Dipert
d6064636d4 Merge pull request #2182 from rstudio/joe/misc/fontawesome-4-note
Add note that Font Awesome support is for 4.7.0
2018-09-14 13:52:30 -07:00
Joe Cheng
9646c9b0a0 Add note that Font Awesome support is for 4.7.0 2018-09-12 08:57:53 -07:00
AliciaSchep
7177618c25 add informative errors when xvar or yvar not in data for nearPoints and brushedPoints 2018-09-04 20:51:51 -07:00
Winston Chang
3bdd4af75c Merge pull request #2168 from rstudio/create-scope-dir
Check for existence of bookmark scope directory before creating
2018-08-24 13:43:15 -05:00
Winston Chang
98d4b5e487 Check for existence of bookmark scope directory before creating 2018-08-24 12:43:45 -05:00
Joe Cheng
8b5639bfdb Tweaks to NEWS 2018-08-24 10:41:08 -07:00
Joe Cheng
1c70b8b1bf Merge pull request #2147 from nathancday/master
Fixes #174, allowing specific days of the week to be disabled.
2018-08-24 10:39:12 -07:00
Joe Cheng
b5a7e03879 Merge branch 'master' into master 2018-08-24 10:38:44 -07:00
Winston Chang
32913f9d95 Merge pull request #2160 from rstudio/digest-xxhash
Use xxhash64 instead of sha256 for hash algorithm
2018-08-17 11:59:47 -05:00
Winston Chang
cbabf9a2a3 Use xxhash64 instead of sha256 for hash algorithm 2018-08-16 15:54:54 -05:00
Winston Chang
03e92c3336 Update NEWS 2018-08-10 21:18:05 -05:00
Winston Chang
997c39fdc0 Merge pull request #2125 from rstudio/plot-interact-scaled
Fix plot interaction for scaled plots
2018-08-10 21:13:16 -05:00
Winston Chang
bba2d1ee18 Grunt 2018-08-10 19:42:11 -05:00
Winston Chang
a60301810f Update coordmap tests 2018-08-10 19:42:11 -05:00
Winston Chang
6b261f76b1 Bump version and update NEWS 2018-08-07 15:25:31 -05:00
Winston Chang
3db5f21d90 Update data structure comment 2018-08-07 15:11:43 -05:00
Winston Chang
121bfcb984 Import old brush after image has loaded 2018-08-07 15:11:43 -05:00
Winston Chang
265de66946 Make sure not to have multiple reset event handlers 2018-08-07 14:51:23 -05:00
Winston Chang
79c5c9f95e Add isnan() function for IE 2018-08-07 14:51:23 -05:00
Winston Chang
3354a47e8a Add width/height to coordmap instead of using naturalWidth/Height
This eliminates the need to use an on load callback.
2018-08-07 14:51:23 -05:00
Winston Chang
a1e1416d7a More consistent use of img to css conversion functions 2018-08-07 10:48:42 -05:00
Winston Chang
24b7a9907f renderCachedPlot: add note about interactive plots to help page 2018-08-07 10:48:42 -05:00
Nate
214abd0cd4 moved new dateInput arguments to last and added formatting conditional if datesdisabled is Date object 2018-08-07 10:16:48 -04:00
Joe Cheng
0bb53e8ca5 Inputs in renderUI/uiOutput don't work with bookmarks (#2139)
* hasCurrentRestoreContext returns FALSE from server side

Fixes #2138.

* Add NEWS item for renderUI bookmarking fix
2018-08-06 15:04:16 -07:00
Winston Chang
ec12caaeba Include x and y pixelratio in coordinfo 2018-08-06 12:51:08 -05:00
Winston Chang
5bbf2aa57a Use canonical CSS property name
Firefox doesn't support shorthand properties like "border-left", but instead
requires "border-left-width".
2018-08-06 12:51:08 -05:00
Winston Chang
84ad9997da Reposition div when resized (without new image) 2018-08-06 12:51:08 -05:00
Winston Chang
9f6ce87443 Remove redundant isEquivalent function 2018-08-06 12:51:08 -05:00
Winston Chang
1ff6c382bf Remove unnecessary ggplot2 workaround 2018-08-06 12:51:07 -05:00
Winston Chang
c366c10ae1 Initialize coordmap only after image loads 2018-08-06 12:51:07 -05:00
Winston Chang
950df1e25c Add support for scaled images and brushing 2018-08-06 12:51:07 -05:00
Winston Chang
909bfa8c14 Allow plot interaction to handle scaled images 2018-08-06 12:51:07 -05:00
Winston Chang
598b48d078 DiskCache: use mtime instead of atime, check for time resolution (#2146)
* DiskCache: check for atime support while running

* Use mtime instead of atime

* Remove mtime resolution checks
2018-08-06 10:50:05 -07:00
Nate
4c7b7f236a added datesdisabled parameter to dateInput() allows disabling of specific yyyy-mm-dd format strings 2018-08-04 19:04:40 -04:00
Nate
896c5b41cb Fixes #174, allowing specific days of the week to be disabled. 2018-08-04 15:47:13 -04:00
Winston Chang
205c35d5e5 Re-document with roxygen2 6.1.0 2018-08-03 17:50:47 -05:00
Winston Chang
bf0dd7d725 Merge pull request #1997 from rstudio/plot-cache
Add renderCachedPlot()
2018-08-03 13:30:13 -05:00
Winston Chang
ba2b811172 Fix argument name in documentation 2018-07-31 15:06:59 -05:00
Winston Chang
be347c3ed4 Don't cache plotResult 2018-07-31 12:07:21 -05:00
Joe Cheng
c01abdb6a9 Merge branch 'master' into plot-cache 2018-07-26 15:52:11 -07:00
Winston Chang
95a5a965a5 Documentation fixes 2018-07-20 16:12:21 -05:00
Winston Chang
fc2849a8ff Remove env and quoted arguments 2018-07-20 16:10:15 -05:00
Winston Chang
fcc900f3e0 Simplify resize logic 2018-07-20 14:48:57 -05:00
Winston Chang
9d0bcd5637 Add shiny-scalable class 2018-07-20 14:44:21 -05:00
Winston Chang
6ebbad5273 Safer file removal order 2018-07-19 23:20:51 -05:00
Winston Chang
930459899a Small logic cleanup 2018-07-19 23:20:40 -05:00
Winston Chang
fe730e2d76 Make session cache public, so that user can set it 2018-07-19 21:57:32 -05:00
Winston Chang
e58b2e9a47 Fix app- and session-level cache initialization 2018-07-19 21:57:07 -05:00
Winston Chang
719dbab0c2 DiskCache: make destroy_on_finalize default to FALSE 2018-07-19 14:35:59 -05:00
Winston Chang
86ea023e2e Update caches from code review feedback 2018-07-19 14:33:53 -05:00
Winston Chang
bc0fb3f44c Restructure drawReactive/renderFunc code 2018-07-18 14:52:39 -05:00
Winston Chang
6d37f6b4dd Set max-width and max-height to 100% for cached plots 2018-07-12 10:55:26 -05:00
Winston Chang
958ab85297 Add exec_missing parameter 2018-07-12 10:55:26 -05:00
Winston Chang
a23f973433 Suppress console logging for renderCachedPlot 2018-07-02 17:04:44 -05:00
Winston Chang
c124256bad Improved logging for DiskCache and MemoryCache 2018-07-02 17:04:28 -05:00
Winston Chang
f1b035bcca Update renderCachedPlot documentation and examples 2018-07-02 15:27:51 -05:00
Joe Cheng
81cc7c591e Merge pull request #2080 from schloerke/barret-freeze-thaw
freeze/thaw reactiveValues with namespace support
2018-06-29 15:58:52 -07:00
Winston Chang
a0ca560c3b Doc entries 2018-06-29 13:24:01 -05:00
Winston Chang
d1f20a9c73 Add 'missing' parameter to get() 2018-06-29 13:23:47 -05:00
Barret Schloerke
013059c5b9 merged in master 2018-06-29 11:35:20 -04:00
Winston Chang
fe6ad235ac Add sentinel value cache miss 2018-06-28 23:58:55 -05:00
Winston Chang
67af26ffe6 When MemoryCache is used, we can cache the displayList 2018-06-26 23:11:25 -05:00
Winston Chang
0fce9de04f Change default DiskCache size 2018-06-26 22:59:32 -05:00
Winston Chang
a8b8df21d6 DiskCache: make default finalizer behavior conditional on whether a temp directory is used 2018-06-26 22:57:35 -05:00
Winston Chang
ab2e304f02 DiskCache: Add info about sharing across processes 2018-06-26 22:56:59 -05:00
Winston Chang
574f2c53d4 DiskCache: Make destroy() work robustly with multiple processes 2018-06-26 22:26:27 -05:00
Winston Chang
bc85d812d2 DiskCache: Avoid errors from a race condition 2018-06-26 16:38:43 -05:00
Winston Chang
364990a29f Fix .rds directory search pattern 2018-06-26 16:37:58 -05:00
Winston Chang
9ac9e36873 DiskCache: Check if cache has been destroyed by someone else 2018-06-26 15:48:08 -05:00
Winston Chang
6745e09688 Add support for promisey cache key 2018-06-25 21:13:55 -05:00
Winston Chang
e758927c84 DiskCache: Add warning when caching reference objects 2018-06-25 15:53:47 -05:00
Winston Chang
90fbf7d50f Add comment about pruning 2018-06-25 15:34:43 -05:00
Winston Chang
75f1ee0082 Prune after setting value 2018-06-25 15:30:30 -05:00
Winston Chang
750aaf451a NEWS edits 2018-06-25 14:11:42 -05:00
Winston Chang
b44bfe9109 Grunt 2018-06-25 14:10:22 -05:00
Winston Chang
aa392f8563 Merge pull request #2102 from tmastny/selectize-remote
Resolves #1933: Serve-side selectize extended to all inputs
2018-06-25 14:07:56 -05:00
Winston Chang
ac7228f6c4 Merge branch 'master' into selectize-remote 2018-06-25 14:07:20 -05:00
Winston Chang
dcb12addaa Merge pull request #2108 from tmastny/par-oma
Resolves #1935: Fix coordinate outputs when modifying outer margins
2018-06-25 14:05:39 -05:00
Winston Chang
ad398b5f8a Merge branch 'master' into par-oma 2018-06-25 14:05:30 -05:00
Timothy Mastny
803cb4806e add new item 2018-06-25 14:04:31 -05:00
Timothy Mastny
1a468bbb61 add news items 2018-06-25 14:02:18 -05:00
Winston Chang
c332c051f3 Merge pull request #2099 from tmastny/slider-type
Fix #2019: `updateSliderInput` changes formatting
2018-06-25 14:00:18 -05:00
Timothy Mastny
db48befcb7 removed TODO comments 2018-06-25 13:51:24 -05:00
Winston Chang
b02edb05ac DiskCache: Use temp file when setting value 2018-06-25 11:56:01 -05:00
Winston Chang
d7009fd1c8 DiskCache: Don't call exists() before reading file 2018-06-25 11:55:29 -05:00
Winston Chang
ce3755676c Fix files that were split 2018-06-25 10:45:12 -05:00
Winston Chang
db3c1b728d Merge branches 'plot-cache-split-1' and 'plot-cache-split-2' into plot-cache 2018-06-25 10:42:52 -05:00
Winston Chang
1761de4740 Rename cache.R cache-context.R 2018-06-25 10:42:49 -05:00
Winston Chang
09d496925b Rename cache.R cache-memory.R 2018-06-25 10:41:49 -05:00
Winston Chang
3af5327f1c Rename cache.R cache-disk.R 2018-06-25 10:41:04 -05:00
Winston Chang
06cb14d7ec Rename $has method to $exists 2018-06-22 23:27:49 -05:00
Winston Chang
7be1a9d7fa Add memoryCache and make it the default 2018-06-22 23:08:39 -05:00
Winston Chang
95243fb35c DiskCache: Add LRU eviction policy 2018-06-22 22:44:27 -05:00
Winston Chang
26438a3979 DiskCache cleanup 2018-06-22 19:32:29 -05:00
Winston Chang
28db097a71 Use promise domain for currentOutputName 2018-06-22 19:31:20 -05:00
Winston Chang
76fdd8ae04 Make renderCachedPlot work with async 2018-06-22 16:47:55 -05:00
Timothy Mastny
3a73bfb142 changed output coordinate system to "ndc" to account for margin changes 2018-06-21 17:48:16 -05:00
Timothy Mastny
a24bdabf08 Updates to Winston's feedback: removed for ... of iteration that is not supported in IE. 2018-06-21 16:17:42 -05:00
Winston Chang
8815f293a2 Update R version check information 2018-06-21 15:55:35 -05:00
Winston Chang
9af2775539 Fix absolutePath to correctly handle absolute paths 2018-06-21 15:48:54 -05:00
Winston Chang
ae5deae6e9 Use output name in cache key 2018-06-21 14:55:30 -05:00
Winston Chang
61c2126498 Add diskCache function, and app- and session-level caches 2018-06-21 14:44:09 -05:00
Timothy Mastny
881fe0cfce explicitly set number to null instead of implicitly for better documentation 2018-06-21 14:30:15 -05:00
Timothy Mastny
a999bf389c update NEWS.md 2018-06-21 14:30:15 -05:00
Timothy Mastny
ff3b97b630 refactored data type checking for consistency 2018-06-21 14:30:15 -05:00
Timothy Mastny
639b520d39 updateInputSlider can now change from date to date-time formatting. fixes #2019 2018-06-21 14:30:15 -05:00
Timothy Mastny
19dc29ea17 changes as per Winston's feedback; additional comments, and edge cases, and removed unnecessary JS. 2018-06-21 12:47:14 -05:00
Timothy Mastny
97bebae8d7 fixed default selection for multiple-select 2018-06-20 15:41:35 -05:00
Timothy Mastny
cf534ce6da remove "thiz"s and replace with explicit "selectize" 2018-06-20 15:41:35 -05:00
Timothy Mastny
f25f691a55 fix selectize for default selected value NULL 2018-06-20 15:41:35 -05:00
Timothy Mastny
cbebf8be7b improve performance of R input processing 2018-06-20 15:41:35 -05:00
Timothy Mastny
165ce26b2f Fixes #1933. Fixed JSON encoding of input data, and added more optgroup controls on JS-side 2018-06-20 15:41:35 -05:00
Winston Chang
572c863bff Merge pull request #2106 from schloerke/js_patch
compile with grunt to get version in js code (v1.1.0.9000)
2018-06-20 12:47:43 -05:00
Barret Schloerke
d3c85d67b8 gruntfile should run 'default' task to make sure everything is built like normal 2018-06-20 13:31:10 -04:00
Barret Schloerke
ff3434f77e add a R test to make sure shiny.js {{ VERSION }} was replaced 2018-06-20 12:53:07 -04:00
Barret Schloerke
762528c044 add a grunt test to make sure {{ VERSION }} was replaced 2018-06-20 12:52:46 -04:00
Barret Schloerke
1891af0d4a compile with grunt 2018-06-20 11:39:12 -04:00
Winston Chang
583ad036f7 Streamline renderCachedPlot API 2018-06-19 16:00:49 -05:00
Winston Chang
ac92bf98d4 WIP 2018-06-18 16:25:36 -05:00
Winston Chang
fd90ff7ff7 Use DiskCache class 2018-06-18 16:25:36 -05:00
Winston Chang
d06dbbe5db Change cacheResetEventExpr to cacheResetExpr 2018-06-18 16:25:36 -05:00
Winston Chang
bffc4995d7 Rename normalizePath2 to absolutePath 2018-06-18 16:25:36 -05:00
Winston Chang
4b8b406bed Add sizeGrowthRatio function 2018-06-18 16:25:36 -05:00
Winston Chang
5641153272 renderCachedPlot: pass ... args 2018-06-18 16:25:36 -05:00
Winston Chang
08c6c7781f Rename cacheClearExpr to cacheResetEventExpr 2018-06-18 16:25:36 -05:00
Winston Chang
ad2ad391a7 Isolate user code 2018-06-18 16:25:36 -05:00
Winston Chang
caac88be0d Don't re-run user code when only dimensions change 2018-06-18 16:25:36 -05:00
Winston Chang
10660aa373 Rename cacheInvalidationExpr to cacheClearExpr 2018-06-18 16:25:36 -05:00
Winston Chang
cfaf97aee4 Add entries to staticdocs 2018-06-18 16:25:36 -05:00
Winston Chang
55f14576f0 Remove createCachedPlot function 2018-06-18 16:25:36 -05:00
Winston Chang
4dca94ac99 Code reorganization 2018-06-18 16:25:36 -05:00
Winston Chang
14779d3d27 Only invalidate plot when fitted dimensions change 2018-06-18 16:25:36 -05:00
Winston Chang
66d1e710b5 Allow renderCachedPlot to take a directory for scope 2018-06-18 16:25:36 -05:00
Winston Chang
12ae3c17e9 Allow onStop to take a NULL session 2018-06-18 16:25:35 -05:00
Winston Chang
36e4da0709 Add app/session scoping for renderCachedPlot 2018-06-18 16:25:35 -05:00
Winston Chang
91631cb081 Note bug fix in R 2018-06-18 16:25:35 -05:00
Winston Chang
224f082e1f Implement cache invalidation 2018-06-18 16:25:35 -05:00
Winston Chang
76b239a6ea Convert renderCachedPlot to take expr and cacheKeyExpr 2018-06-18 16:25:35 -05:00
Winston Chang
cb476b510d Initial implementation of renderCachedPlot 2018-06-18 16:25:35 -05:00
Winston Chang
334f233968 Move plot caching code into separate file 2018-06-18 16:25:35 -05:00
Winston Chang
e1f21250b9 Rename plotCache to createCachedPlot 2018-06-18 16:25:35 -05:00
Winston Chang
8d087e4f20 Get output info and auto-size plot cache images 2018-06-18 16:25:35 -05:00
Winston Chang
9e35e8c947 Allow user code to pass width/height/pixelratio 2018-06-18 16:25:35 -05:00
Winston Chang
f98faef024 Document cache scoping and minor code cleanup 2018-06-18 16:25:35 -05:00
Winston Chang
0f9346ead5 Add different scoping levels for plotCache 2018-06-18 16:25:35 -05:00
Winston Chang
fc8118c694 First implementation of plotCache 2018-06-18 16:25:35 -05:00
Joe Cheng
026b7278c1 Merge pull request #2091 from schloerke/barret-varSelectInput
Variable Select Input
2018-06-14 09:39:09 -05:00
Barret Schloerke
375a7e7e5c respect existing class values and add tests 2018-06-11 10:31:49 -05:00
Barret Schloerke
7a1aecb1a4 varSelectInput doc tweaks 2018-06-08 10:59:45 -04:00
Barret Schloerke
b3690e8680 add bullets in details for output return value (update corresponding example) 2018-06-08 10:51:37 -04:00
Barret Schloerke
97d490cfb4 remove rogue staticdocs fn 2018-06-07 16:52:35 -04:00
Barret Schloerke
2081dda6fc merged master -> varSelectInput 2018-06-07 16:46:31 -04:00
Barret Schloerke
ea912fc50c staticdocs for varSelectInput 2018-06-07 16:43:20 -04:00
Barret Schloerke
b655fdf68f added news for varSelectInput 2018-06-07 16:43:05 -04:00
Barret Schloerke
4749f46a4f add shiny.symbol and shiny.symbolList tests 2018-06-07 16:10:47 -04:00
Barret Schloerke
f95bb9c82d compile and document 2018-06-07 16:10:24 -04:00
Barret Schloerke
6529529cdb add new input 'varSelectInput()' and input binding 'shiny.symbol' and 'shiny.symbolList' 2018-06-07 16:10:01 -04:00
Tim Mastny
3a2a3f21d4 Improve optgroup documentation per #1864 (#2084)
* improves optgroup documentation per #1864

* change PR number in NEWS.md

* change addresses to addressed
2018-06-07 12:09:45 -05:00
Tim Mastny
631bc1c481 Addresses #2042: lighten dates outside of range for datepicker (#2087)
* dehighlight dates outside of range for datepicker

* override color in shiny.css instead of package css

* refactored css styling to use specific references instead of !important
2018-06-07 11:57:27 -05:00
Barret Schloerke
597af36759 added news item 2018-05-31 10:31:35 -04:00
Barret Schloerke
691062f687 white space 2018-05-31 10:30:19 -04:00
Barret Schloerke
6651c4ea48 when freezing a reactivevalues key, use the name space similar to $.reactivevalues 2018-05-31 10:23:30 -04:00
Barret Schloerke
116559e5a0 use utils namespace 2018-05-31 10:21:55 -04:00
Barret Schloerke
7818e8ed64 white space 2018-05-31 10:21:42 -04:00
Winston Chang
2880391620 runApp: add support for IPv6 addresses 2018-05-25 16:19:51 -04:00
Joe Cheng
f742605a1b Bump version to *.9000. Back to work! 2018-05-17 17:20:17 -07:00
Winston Chang
2afff67e89 Bump version to 1.1.0 2018-05-16 15:18:59 -07:00
Winston Chang
fe7bd53250 Bump httpuv version and add NEWS note 2018-05-16 15:18:59 -07:00
Joe Cheng
6df3509869 Merge pull request #2062 from rstudio/joe/bugfix/windows-rounding
Fix #2061: Tests failing on Windows due to rounding errors
2018-05-16 15:17:04 -07:00
Joe Cheng
062dc771aa Fix #2061: Tests failing on Windows due to rounding errors 2018-05-16 14:44:34 -07:00
Joe Cheng
d4688db31c Update NEWS 2018-05-03 14:08:52 -07:00
Joe Cheng
c49a289619 Fix #2033: Rstudio Viewer window not closed on shiny::stopApp() 2018-05-03 14:06:34 -07:00
Joe Cheng
9c3a0c86ca Take dependency on later >=0.7.2 2018-05-01 20:37:25 -05:00
Winston Chang
01b24e984c Merge pull request #2038 from rstudio/joe/bugfix/cycle-start-bugs
Fix #2037: With enableBookmarking="url", clientData is not available …
2018-04-25 13:34:34 -05:00
Joe Cheng
9dd4302fe9 Fix #2037: With enableBookmarking="url", clientData is not available when observers are first run
Also fixed reactiveTimer firing even while async tasks are active
2018-04-25 10:54:00 -07:00
Winston Chang
c2f03aa833 Merge pull request #2036 from rstudio/joe/misc/renderui-experimental
Remove "experimental feature" tag from renderUI
2018-04-24 12:42:04 -05:00
Barbara Borges Ribeiro
2260459422 brought observeEvent/eventReactive documentation up to date 2018-04-24 17:44:54 +01:00
Winston Chang
e838cc3fe9 Re-document 2018-04-24 10:24:52 -05:00
Winston Chang
74457b95e9 NEWS edits 2018-04-24 10:17:03 -05:00
Joe Cheng
d5754515a6 Remove "experimental feature" tag from renderUI 2018-04-23 17:05:08 -07:00
Joe Cheng
4ed13c04f5 Merge pull request #2032 from rstudio/fix-flushed-callbacks
Set default reactive domain when executing flushedCallbacks. Fixes #1975
2018-04-21 10:02:10 -07:00
Winston Chang
5a5294cc44 Set default reactive domain when executing flushedCallbacks. Fixes #1975 2018-04-21 11:46:50 -05:00
Joe Cheng
3a5d48ae7c Remove outdated Remotes
Live code review by @wch
2018-04-19 14:59:10 -07:00
Joe Cheng
6b605804d2 Upgrade Selectize to 0.12.4 2018-04-19 14:19:12 -07:00
Joe Cheng
ffe883ab72 Merge pull request #2022 from rstudio/joe/bugfix/timer-leak
Fix #2021: Memory leak with reactiveTimer and invalidateLater
2018-04-19 14:16:35 -07:00
Joe Cheng
31c4e0fdfe Add test to demonstrate vectorized unscheduling 2018-04-19 12:52:30 -07:00
Joe Cheng
66f970e0bd Merge pull request #2026 from rstudio/fix-ggplot2
Fix plot coordmap for devel version of ggplot2. Closes #2016
2018-04-19 12:46:38 -07:00
Winston Chang
07b223dcb0 Fix plot coordmap for devel version of ggplot2. Closes #2016 2018-04-19 11:40:26 -05:00
Joe Cheng
f1e27b6ffb Fix #1922: Warning: partial match of 'y' to 'yintercept' 2018-04-19 10:00:24 -05:00
Joe Cheng
389463aea5 Merge branch 'joe/feature/undedupe-inputs' 2018-04-18 19:33:37 -07:00
Joe Cheng
b11ab9a31c Update NEWS 2018-04-18 12:53:31 -07:00
Joe Cheng
5fe85b07b7 Merge remote-tracking branch 'andrewsali/master' 2018-04-18 12:52:40 -07:00
Joe Cheng
3c7b1e7d21 Update NEWS 2018-04-18 12:32:16 -07:00
Joe Cheng
c556cf1e69 Fix #2021: Memory leak with reactiveTimer and invalidateLater 2018-04-18 12:30:14 -07:00
Joe Cheng
722e5fb5f7 Modify internal JS to use Shiny.setInputValue, {priority: "event"} 2018-04-18 12:05:18 -07:00
Joe Cheng
e90cc591b7 Update NEWS 2018-04-18 11:57:55 -07:00
Joe Cheng
c555725201 Change {immediate: ...} to {priority: "deferred|immediate|event"}
This was the product of a long discussion between @wch, @alandipert, @bborgesr
and myself. The conflation of immediate (no throttle/debounce) and non-dedupe
in a single "immediate" flag was deemed unacceptable, because UI controls often
want immediacy but also dedupe. Introducing a second "dedupe" flag would work
but {immediate: false, dedupe: false} doesn't make much sense, and dedupe not
only implies that InputNoResendDecorator should behave differently but also
InputBatchSender (i.e. no deduplication AND no coalescing).

We decided to remove the "immediate" boolean option and replace it with a
string option that would have three possibilities at this time. The only con
to this approach is if anyone is calling onInputChange with immediate:true
today, and I can't imagine anyone is. The immediate flag only has any effect
if the input id that's being set has been put in debounce/throttle mode, and
I don't even think that is documented today, and I'm not even sure it's
possible to do it from custom JS (that's not part of a custom input binding).
2018-04-17 16:39:05 -07:00
Joe Cheng
cef1f3c7ee withReactiveDomain now acts as a promise domain
Without this change, async handlers won't return any
value for getDefaultReactiveDomain().

    library(shiny)
    library(promises)

    ui <- fluidPage(
      p("This app tests if async handlers have reactive domains. You'll get a yes/no answer below."),
      h3(
        "Does it work?",
        textOutput("answer", inline = TRUE)
      )
    )

    server <- function(input, output, session) {
      output$answer <- renderText({
        promise_resolve(TRUE) %...>% {
          if (!is.null(getDefaultReactiveDomain()))
            "Yes!"
          else
            "No :("
        }
      })
    }

    shinyApp(ui, server)
2018-04-16 20:51:46 -05:00
Joe Cheng
e5d1fa1ea4 Fix #2008: Allow eventReactive and observeEvent eventExprs to be async (#2014)
* Fix #2008: Allow eventReactive and observeEvent eventExprs to be async

This makes it possible to monitor e.g. async reactives.

In the process of fixing this, also discovered that observers don't
filter out shiny.silent.error (i.e. req(FALSE)) when they come back
from async operations. For example, this will kill the current
Shiny session instead of being ignored:

  observe({
    promise_resolve(TRUE) %...>%
      {req(FALSE)}
  })

This issue is also fixed in this commit.

* Enable deep stack trace by default, now that it's fast
2018-04-16 20:50:28 -05:00
Joe Cheng
3ccf2937b4 Fix #928: allow inputs to trigger reactive flow even if the value of input hasn't changed
We already had an `immediate` input option, which was used to override client side rate
limiting mechanisms (debounce/throttle). This commit extends the semantics of that option
to also mean that duplicate values should not be ignored on the client side.

Previous to this commit, circumventing the client side dedupe logic was not enough. The
server side ReactiveValues object was also subject to deduping. With this commit, the
low-level ReactiveValues class's constructor now has a `dedupe` option, which defaults
to TRUE; the ReactiveValues used for a session's input has it turned to FALSE. I figure
if I had to work this hard to get the client to stop sending duplicates, and the input
values are only expected to ever be updated by the client, then there's really no reason
for server side deduping to be performed for this particular ReactiveValues object.

It would make sense as a future feature to also make deduping optional for user-created
reactiveValues and reactiveVal objects.
2018-04-16 18:37:47 -07:00
Joe Cheng
b7b696630f Fix #2003: Long stack traces are truncated 2018-04-16 17:16:12 -05:00
Joe Cheng
84aba546bc Fix #2000: Implicit calls to xxxOutput not working inside modules (#2010)
* Fix #2000: Implicit calls to xxxOutput not working inside modules

* Add comment, update NEWS

* Credits in NEWS
2018-04-16 16:57:13 -05:00
Winston Chang
741236df56 Merge pull request #2011 from rstudio/joe/feature/output-null
Fix #1989: Allow outputs to be removed by assigning NULL to them
2018-04-12 16:29:48 -05:00
Winston Chang
e3584f0a61 Merge pull request #2013 from rstudio/joe/bugfix/render-plot-args
renderPlot's ... args were being dropped
2018-04-12 16:27:46 -05:00
Joe Cheng
432482c5a7 renderPlot's ... args were being dropped 2018-04-12 11:57:32 -07:00
Joe Cheng
323ad46bba Implement #1989: Allow outputs to be removed by assigning NULL to them 2018-04-11 18:40:32 -07:00
Alan Dipert
ace0fe1802 Merge pull request #2005 from rstudio/alan/bugfix/dndfix
Fix dragging and dropping in the presence of jQuery 3.0
2018-04-11 11:53:42 -07:00
Alan Dipert
36f244fece Merge branch 'master' into alan/bugfix/dndfix 2018-04-11 11:52:23 -07:00
Joe Cheng
99e5ef99ec Move some bullets around 2018-04-10 10:08:15 -07:00
Alan Dipert
d6d3ed5bbc NEWS 2018-04-10 08:40:40 -07:00
Alan Dipert
49d09ecf30 Grunt 2018-04-10 08:30:46 -07:00
Alan Dipert
c529a03096 DnD: Fix in the presence of jQuery 3.0 (removed .size()) 2018-04-10 08:29:03 -07:00
Andras Sali
101d9aa0fa Move trigger after value change 2018-04-07 16:51:03 +02:00
Andras Sali
b4864e1180 Trigger shiny:value even if same data is received. Fixes #1978 2018-04-07 16:46:34 +02:00
Winston Chang
cba7304ab9 Merge pull request #1996 from rstudio/fix-selectize
fix selectize capitalization (regression introduced by #1861)
2018-04-05 11:58:20 -05:00
Barbara Borges Ribeiro
2d058b0519 move attribute setting to after choices is reassigned (this was getting lost after the changes in #1861). 2018-04-04 19:55:15 +01:00
Barbara Borges Ribeiro
eed9231884 fix selectize capitalization (regression introduced by #1861) 2018-04-04 14:00:44 +01:00
Joe Cheng
5c84eaf2a5 Merge pull request #1990 from rstudio/joe/feature/better-stacks
Better stack traces
2018-03-27 19:31:51 -07:00
Joe Cheng
2ef7226be0 Use seq_along instead of 1:length(x)
It behaves when length(x) == 0
2018-03-27 18:30:24 -07:00
Joe Cheng
e5d1c61cdf Merge branch 'master' into joe/feature/better-stacks 2018-03-27 18:04:05 -07:00
Joe Cheng
e635055ab8 Update NEWS 2018-03-27 18:02:41 -07:00
Joe Cheng
d8d4e3b262 Don't error when entire stack trace is stripped/pruned 2018-03-27 16:35:45 -07:00
Joe Cheng
8f29543479 Use qualified name for utils::tail (R CMD check NOTE) 2018-03-27 16:19:28 -07:00
Joe Cheng
c11a8ea24b Fix tests 2018-03-27 15:35:29 -07:00
Joe Cheng
86646d7faa Make srcref offsetting optional 2018-03-27 15:35:06 -07:00
Joe Cheng
6e44915e08 Merge pull request #1984 from rstudio/joe/feature/faster-deep-stacks
Lazily format stack traces
2018-03-27 10:31:00 -07:00
Joe Cheng
f8b99cf4e9 Add deprecation docs 2018-03-26 11:38:12 -07:00
Joe Cheng
0e7d6ff192 Refactoring and deprecation in conditions.R
- Refactor printError so a working printStackTrace falls out
- Deprecate extractStackTrace and formatStackTrace, see if anyone uses them
2018-03-26 11:35:50 -07:00
Barbara Borges Ribeiro
66501dac97 Add new autoclose = TRUE param to dateInput() and dateRangeInput (#1987)
* Add new `autoclose = TRUE` param to both dateInput() and dateRangeInput()

* added NEWS item
2018-03-23 09:40:19 -07:00
Joe Cheng
195907b2ec printError implements lobstr::cst analysis and deep-stack-aware frame suppression 2018-03-22 12:22:46 -07:00
Joe Cheng
be11b44864 First steps to improved stack traces
- Adds functions we will need for tracking ..stacktraceon/off..
  across deep stacks
- Adds functions we will need for pruning according to lobstr::cst
  logic

These functions are not yet integrated, that will occur in a
separate commit.
2018-03-20 16:45:47 -07:00
Joe Cheng
bc7cd21c13 Update NEWS.md 2018-03-20 16:39:52 -07:00
Carson Sievert
0555cbdd28 relay offsetWidth/offsetHeight of htmlwidgets to clientData (#1981)
Addresses #1980
2018-03-20 16:37:43 -07:00
Joe Cheng
97498451bb Lazily format stack traces
With deep stack traces enabled, whenever then() is called, we need
to grab the current stack, just in case a downstream callback throws
an error and we need to form a deep stack trace.

Previously, we were calling formatStackTrace at the time that we
grab the current stack (i.e. no error has happened yet) because I
wasn't sure whether holding a reference to sys.calls() for a long
time was a good idea from a garbage collection perspective; would it
prevent the stack frame environments from being collected? But the
answer is no, sys.calls() is just calls, which can be confirmed with
.Internal(inspect(sys.calls()).

By deferring the formatStackTrace call to when we actually need to
print the stack trace, we save ourselves a ton of work--it turns out
it's quite expensive to format the stack traces, much more expensive
than sys.calls() alone.
2018-03-20 12:35:17 -07:00
Joe Cheng
2e0d9b5475 Bump httpuv dependency version 2018-03-18 19:07:43 -07:00
Barbara Borges Ribeiro
62395f3103 Improve error handling when addResourcePath() fails (especially for runtime: shiny_prerendered documents) (#1968)
* A copy of yihui's PR for rmarkdown (https://github.com/rstudio/rmarkdown/pull/1171/) to avoid to error "Error in normalizePath: path[1]="": No such file or directory" when running any tutorial

* first try

* limited the scope of the `tryCatch` wrapper to the one important line that needed it; added news item
2018-03-16 15:36:12 -07:00
Winston Chang
6b31cd6aee Merge pull request #1965 from rstudio/joe/bugfix/plot-dim-error
Fix #1964: renderPlot cache breaks when width/height throw
2018-03-01 11:12:56 -06:00
Joe Cheng
e67a8ba369 Fix #1964: renderPlot cache breaks when width/height throw
Fixed by moving the isolate(getDims()) call into the (effectively)
try/catch that does a non-isolated getDims() if an error occurs
2018-02-28 15:40:55 -08:00
Joe Cheng
133d301925 Merge pull request #1961 from rstudio/fix-date-sliders-bookmarking
Fix URL-encoded bookmarking with date/date-time sliders
2018-02-28 14:26:50 -08:00
Joe Cheng
17c40a5d1d Merge pull request #1960 from rstudio/slider-formatting
Don't show commas after decimal mark in sliderInput
2018-02-28 13:01:20 -08:00
Winston Chang
042211e5f6 Grunt 2018-02-28 14:34:09 -06:00
Winston Chang
d12830d700 sliderInput: don't show commas after decimal 2018-02-28 14:33:42 -06:00
Winston Chang
b411c70280 Fix URL-encoded bookmarking with date/date-time sliders 2018-02-27 20:42:11 -06:00
Winston Chang
2bc22cc7d5 Merge pull request #1955 from rstudio/update-slider
Update ion.rangeSlider to 2.2.0
2018-02-27 13:21:09 -06:00
Joe Cheng
b4c189c89b Merge pull request #1956 from rstudio/fix-slider-rounding
Avoid rounding errors in sliderInput
2018-02-27 10:55:11 -08:00
Winston Chang
fe3f351a2d Avoid rounding errors from pretty(). Fixes #1006 2018-02-27 10:50:36 -06:00
Winston Chang
076be9cba7 Remove unused keyboard_step parameter
keyboard_step was removed in ion.rangeSlider 2.2.0.
2018-02-26 15:57:30 -06:00
Winston Chang
f28dcd85fb Update to ion.rangeSlider 2.2.0 2018-02-26 15:57:30 -06:00
Joe Cheng
8e0f17c9d7 Merge pull request #1954 from rstudio/fix-bookmark-restore
Look for restore context associated with session
2018-02-26 09:25:33 -08:00
Winston Chang
d73817a0db Look for restore context associated with session. Fixes #1948 2018-02-26 10:55:38 -06:00
Joe Cheng
11874db825 Remove background-thread branch from httpuv remote 2018-02-16 15:41:32 -08:00
Joe Cheng
5d5a43ce90 Merge pull request #1932 from rstudio/async
Async
2018-02-16 07:41:36 -08:00
Winston Chang
75e548caab For installation of Rtools on Appveyor 2018-02-16 09:15:40 -06:00
Joe Cheng
c901e7ba06 Update TODO-promises.md 2018-02-13 15:23:06 -08:00
Joe Cheng
b1dc3dfca1 Restore label to plotObj reactive 2018-02-09 11:12:46 -08:00
Joe Cheng
ce4ed20c69 Fix remaining failing tests 2018-02-09 11:12:46 -08:00
Joe Cheng
d44df7f860 Stack traces were being lost inside hybrid_chain 2018-02-09 11:12:46 -08:00
Joe Cheng
54353e0e1f Fix coordmap unit tests 2018-02-09 11:12:46 -08:00
Joe Cheng
1c042b6efb Add to .Rbuildignore 2018-02-09 11:12:46 -08:00
Joe Cheng
b8df1f29c4 Remove unused function 2018-02-09 11:12:46 -08:00
Joe Cheng
18252f5b03 Use later >= 0.7.1 2018-02-09 11:12:46 -08:00
Joe Cheng
881370f284 Remove extraneous comments 2018-02-09 11:12:46 -08:00
Joe Cheng
35d1747bc3 Don't allow invalidation from a child process 2018-02-09 11:12:46 -08:00
Joe Cheng
91ac89a54e Update TODOs 2018-02-09 11:12:46 -08:00
Joe Cheng
3c694d9bd9 More robust process identification (thanks @HenrikBengtsson) 2018-02-09 11:12:46 -08:00
Joe Cheng
6a78e9df77 Detect (probably inadvertent) attempts to inherit reactive contexts across processes
Example (we want this to fail):

library(shiny)
library(future)
plan(multicore)

r <- reactiveVal(TRUE)
isolate({
  f <- future({
    r()
  })
  value(f)
})
2018-02-09 11:12:45 -08:00
Joe Cheng
078c6eb30a Add TODO 2018-02-09 11:12:45 -08:00
Joe Cheng
d35c6002a6 Respect pixelratio (retina) when redrawing cached images 2018-02-09 11:12:45 -08:00
Joe Cheng
f23fc3beaa Plots were not respecting pixel ratio (retina) 2018-02-09 11:12:45 -08:00
Joe Cheng
5a352e5ace Update TODOs 2018-02-09 11:12:45 -08:00
Joe Cheng
27cae0065e Fix bug where req(cancelOutput=TRUE) would leave things grey
This was introduced by some changes to shinyapp.js that were necessary
before async outputs and sync outputs were held/flushed together. Now
that async/sync outputs are held/flushed together, these changes are
not necessary and removing them fixes the problem.

The test app is in shiny-examples/205-async-req. I also moved a test
app from manualtests/async/download.R to shiny-examples/204-async-download.
2018-02-09 11:12:45 -08:00
Joe Cheng
50be2993fa Add TODO 2018-02-09 11:12:45 -08:00
Joe Cheng
d9ea15e9bc Update TODOs 2018-02-09 11:12:45 -08:00
Joe Cheng
03b1d45d7e Make deep stack traces opt-in; fix imports 2018-02-09 11:12:45 -08:00
Joe Cheng
e48d6878c4 Update TODOs 2018-02-09 11:12:45 -08:00
Joe Cheng
1a3b255848 Update TODOs 2018-02-09 11:12:45 -08:00
Joe Cheng
f00aa94d7e Suspend session during async download operation 2018-02-09 11:12:45 -08:00
Joe Cheng
f7980b19f4 Update TODOs 2018-02-09 11:12:45 -08:00
Joe Cheng
6a1f9677a5 Support async downloadHandler content functions
If a downloadHandler content function returns a promise (or future)
then Shiny will wait for the promise to resolve before serving up
the file download.
2018-02-09 11:12:45 -08:00
Joe Cheng
e844bb36a5 Update TODOs 2018-02-09 11:12:45 -08:00
Joe Cheng
ae364adfc2 wip2 2018-02-09 11:12:45 -08:00
Joe Cheng
c14a382b90 wip 2018-02-09 11:12:45 -08:00
Joe Cheng
da9c2beaaf Update TODO 2018-02-09 11:12:45 -08:00
Joe Cheng
a4a56476db Update TODO 2018-02-09 11:12:45 -08:00
Joe Cheng
39d3784b9b async support for renderDataTable 2018-02-09 11:12:45 -08:00
Joe Cheng
7d29df58f1 Support same-tick execution for synchronous outputs 2018-02-09 11:12:45 -08:00
Joe Cheng
05aa413683 promises::finally() was missing namespace prefix 2018-02-09 11:12:45 -08:00
Joe Cheng
132f90f45b Support promise domain wrapSync; fix renderPrint visibility
Also introduce promise_chain and hybrid_chain, for assembling chains of
operations without involving magrittr-style operators
2018-02-09 11:12:45 -08:00
Joe Cheng
4526fd1917 Update TODO 2018-02-09 11:12:45 -08:00
Joe Cheng
2602dc15b0 Changes to flush cycle to support async
- Moved (in|de)crementBusyCount calls out of Context and into Observer
- decrementBusyCount is (effectively) deferred for async observers until
  the async operation is complete
- invalidateLater didn't force(session), almost certainly was buggy
- invalidateLater, reactiveTimer, and manageInputs all now use a new
  session$cycleStartAction, which delays their effect until observers
  (including async ones) are done executing
2018-02-09 11:12:45 -08:00
Joe Cheng
2314f63424 Fix broken .shiny__stdout mechanism 2018-02-09 11:12:45 -08:00
Joe Cheng
c2410600ee Refactor list of TODOs 2018-02-09 11:12:45 -08:00
Joe Cheng
f7e4702685 Restore writing of _n_flushReact and _x_flushReact to stdout 2018-02-09 11:12:45 -08:00
Joe Cheng
71682512c4 Refactor flush cycle 2018-02-09 11:12:45 -08:00
Joe Cheng
20b82fbf77 Cleanup R CMD check 2018-02-09 11:12:45 -08:00
Joe Cheng
631f09847d Update TODOs 2018-02-09 11:12:45 -08:00
Joe Cheng
671585f68a Update TODOs 2018-02-09 11:12:45 -08:00
Joe Cheng
5feed888bb Add promises to remotes 2018-02-09 11:12:45 -08:00
Joe Cheng
47bef0f1b0 Remove extraneous debugging code 2018-02-09 11:12:45 -08:00
Joe Cheng
c1dc662a40 "promise" package was renamed to "promises" 2018-02-09 11:12:45 -08:00
Joe Cheng
16e1721fe8 Add TODO comment 2018-02-09 11:12:45 -08:00
Joe Cheng
f406e13600 Add TODO 2018-02-09 11:12:45 -08:00
Joe Cheng
9063133a7b Fix various rendering bugs 2018-02-09 11:12:45 -08:00
Joe Cheng
3fbb436187 Add call_async temporarily; this should probably live somewhere else 2018-02-09 11:12:45 -08:00
Joe Cheng
7c845d070b Sync to changes in promise. run_now aggressively in serviceApp. 2018-02-09 11:12:45 -08:00
Joe Cheng
5e905aa73e Implement execOnResize 2018-02-09 11:12:45 -08:00
Joe Cheng
e15654f265 Minor renderPlot cleanup 2018-02-09 11:12:45 -08:00
Joe Cheng
369c067efc Update TODO 2018-02-09 11:12:45 -08:00
Joe Cheng
c037e69793 Fix ggplot brushing 2018-02-09 11:12:45 -08:00
Joe Cheng
8c935ff44e Use proper promise:: prefix 2018-02-09 11:12:45 -08:00
Joe Cheng
74bf8b0554 renderPlots works!!! Testing needed. 2018-02-09 11:12:45 -08:00
Joe Cheng
6345972efe Adapt promise domains to handle multiple arguments 2018-02-09 11:11:35 -08:00
Joe Cheng
16242e87a1 Some steps toward renderPlot working. Move to promise package instead of system2.5. 2018-02-09 11:11:34 -08:00
Joe Cheng
8155320ba5 wip 2018-02-09 11:09:28 -08:00
Winston Chang
39a7f63972 Update NEWS 2018-01-29 13:45:23 -06:00
Dmitriy Selivanov
7b72209277 fixes #1859 (#1861)
* style & formatting. fixes #1859

* prepare `choices` in `updateSelectizeInput()` as per discussion in #1861

* remove duplicated block in selectizeJSON, simply `lab` assignement logic
2018-01-29 13:41:30 -06:00
Joe Cheng
cad20a0bfe Merge pull request #1856 from rstudio/wch-do-call
Quote arguments to do.call() for nicer stack traces
2017-10-31 11:28:47 -07:00
Winston Chang
ba8d79f202 Fix version text substitution 2017-10-17 10:51:19 -05:00
Winston Chang
176fe699b9 Point to RStudio Community website instead of shiny-discuss 2017-09-27 15:54:05 -05:00
Winston Chang
213ee7be13 Quote arguments to do.call() for nicer stack traces. Closes #1851 2017-09-26 14:36:19 -05:00
Joe Cheng
48fd869c71 Merge pull request #1848 from rstudio/barbara/fix/icon
Revert back the relative position of the icon and title in tabPanel's and navbarMenu's
2017-09-12 07:34:31 -07:00
Barbara Borges Ribeiro
53e47484e2 reverted the relative positioning of the icon and the title text in navbarMenus and tabPanels back to what it was before Shiny 1.0.5 (fixes #1840) 2017-09-12 12:16:06 +01:00
Winston Chang
dc18b20e5a Don't copy httpuv::decodeURIComponent at build time 2017-09-07 21:31:32 -05:00
Barbara Borges Ribeiro
b4c5debbdf Merge pull request #1844 from rstudio/barbara/fix/reactlog
Changed script tags in reactlog from HTTP to HTTPS
2017-09-07 01:43:46 +01:00
Barbara Borges Ribeiro
771d3d52b9 Changed script tags in reactlog from HTTP to HTTPS in order to avoid mixed content blocking by most browsers (thanks @jekriske-lilly) 2017-09-07 01:34:17 +01:00
Joe Cheng
2a53ac093d Merge pull request #1830 from rstudio/wch-compare-version
Add Shiny.compareVersion() function
2017-09-05 11:37:17 -07:00
Winston Chang
4fa2af72cc Avoid port 6697. Closes #1784 2017-08-28 16:40:51 -05:00
Winston Chang
e512d3cd61 Grunt 2017-08-25 14:46:19 -05:00
Winston Chang
16b7ee3985 Add Shiny.compareVersion() function 2017-08-25 14:46:06 -05:00
Winston Chang
4f3d26c31b Add Shiny.version to Javascript (#1826)
* Add Shiny.version to Javascript

* Grunt
2017-08-23 15:52:16 -05:00
Winston Chang
587bf94d69 Merge tag 'v1.0.5'
Shiny 1.0.5 on CRAN
2017-08-23 15:27:56 -05:00
Winston Chang
635ad77e0d Bump version to 1.0.5 2017-08-23 13:11:59 -05:00
Winston Chang
33258da6c3 Bump version to 1.0.5.9000 2017-08-23 13:07:15 -05:00
Joe Cheng
c2b3c3379d Fix #1824: HEAD request on static files causes app to stop (#1825)
* Fix #1824: HEAD request on static files causes app to stop

The problem was that for HEAD requests specifically, we implement
an explicit Content-Length header (normally we let httpuv figure
out the Content-Length based on the content, but for HEAD we don't
return any content but still want to include the Content-Length).

The Content-Length header was only implemented correctly for string
values, not for raw vectors or file-by-path. This change implements
the value correctly for all currently valid httpuv content.

* Update NEWS

* Code review feedback
2017-08-23 13:01:22 -05:00
Winston Chang
e30fac02ed Add safe wrapper for fromJSON 2017-08-21 19:55:48 -05:00
Winston Chang
e74592a654 Escape a few more characters for conditionalPanel expressions 2017-08-21 14:25:20 -05:00
Joe Cheng
ebd47aa73b Merge pull request #1820 from rstudio/wch-fix-conditionalpanel
Escape newline characters in conditionalPanel expression
2017-08-18 20:49:48 -04:00
Winston Chang
e2d19cbaba Grunt 2017-08-18 17:24:06 -05:00
Winston Chang
1f864a846f Escape newline chars in conditionalPanel expr. Fixes #1818 2017-08-18 17:24:06 -05:00
Winston Chang
fc32c2c944 Clarify that choices must be strings 2017-08-18 11:48:41 -05:00
Winston Chang
279e37f1cb Bump version to 1.0.4.9000 2017-08-18 11:47:19 -05:00
Winston Chang
3f9176176e Merge tag 'v1.0.4'
Shiny 1.0.4 on CRAN
2017-08-14 12:01:26 -05:00
Winston Chang
b3201ccafd Add functions to news item 2017-08-10 14:29:06 -05:00
Winston Chang
2a01a620a9 Add NEWS summary 2017-08-10 12:03:22 -05:00
Winston Chang
6f43cf7b82 NEWS cleanup 2017-08-09 18:49:34 -05:00
Winston Chang
1c6250f9c2 Bump version to 1.0.4 2017-08-09 18:49:34 -05:00
Barbara Borges Ribeiro
650075a9ab Fix appendTab for empty tabsetPanels (#1813)
* fix appendTab for empty tabsetPanels; use spread operator to avoid having to resort to apply; upgrade grunt.

* revert back to `Math.max.apply(null, existingTabIds) + 1;` there's no browser compatibility issues there
2017-08-09 18:45:25 -05:00
Winston Chang
668ee6f24a Add references to issues 2017-08-08 11:16:57 -05:00
Alan Dipert
c456ec2c4c drag/drop-able fileInputs (#1782)
* fileInput WIP: Show dropzones when file dragged over window

- Still need to validate dataTransfer contents

* WIP: Basic functionality working

* wip

* Grunt

* WIP state machine

* WIP generalize FSM to data+multimethod

* WIP multimethod

* WIP draghover

* wip multimethod

* WIP, such refactor

* WIP: rm multimethod

* WIP

* WIP resurrect multimethod

* WIP move draghover functions into input object

* WIP colors: use more muted, bootstrap-esque glows

* Grunt

* WIP: use whenAny, more descriptive args in default test/dispatch fns

* WIP more whenAny

* Grunt

* WIP dont use for...of, requires polyfill

* Grunt

* multimethod improvements, documentation. `equal` function.

* multimethod: simplified equal, removed need for forward decl. docs.

* dox

* multimethod improvements, docs

* minor

* IE 10+ drag/drop, first cut

* Grunt

* use functions not arrows for faux instance methods

* Grunt

* fix uploadDropped call

* Grunt

* cleanup drop handler, fix entry to invalid state via doc drop handler

* Grunt

* IE workaround #293932

* Grunt

* yeeeeeeeeeeessss IE WORKSSSSS

* Cleanup; support activeClass/overClass

* everything basically works everywhere \o/

* revert ability to specify classes, hardcode in JS

* MM fixes

* minor fixes

* Grunt

* DnD: Support dragging directly over zones
- Happens when source window occludes browser window

* woo

* Note Safari bug, use draghover for zones

* merge

* Grunt

* news

* include CSS
2017-08-08 11:12:21 -05:00
Joe Cheng
3b0c390a9e Merge pull request #1794 from rstudio/barbara/tabs
Dynamic tabs
2017-08-04 11:31:50 -07:00
Barbara Borges Ribeiro
b02eb11345 do inputId <- session$ns(inputId) in user facing functions for module functionality (rather than overriding the same functions in makeScope) 2017-08-04 18:02:43 +01:00
Barbara Borges Ribeiro
ed3ba303bc Joe's feedback 2017-08-04 17:56:58 +01:00
Barbara Borges Ribeiro
ee5da1410e make hide and remove work well when we want to hide/remove a tab inside a navbarMenu (or the whole menu) and it is selected (before this commit, it wasn't navigating to the first tab like it is supposed to) 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
494627c6e1 make this PR work for modules 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
82ac112dec added select argument to showTab function 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
40cfff33ff for dynamic tabs, send message on session$onFlush (instead of session$onFlushed) 2017-08-04 15:10:08 +01:00
Joe Cheng
c1c5873912 Abandon nearest neighbor tab-showing logic. Just grab the first tab. 2017-08-04 15:10:08 +01:00
Joe Cheng
c090efd562 Fix bug where last tab being removed, didn't update tabset input value 2017-08-04 15:10:08 +01:00
Joe Cheng
91dbb0e77b htmlDependencies are properly loaded with dynamic tabs 2017-08-04 15:10:08 +01:00
Joe Cheng
dde7b144f0 Add select=FALSE argument to insert/append/prependTab 2017-08-04 15:10:08 +01:00
Joe Cheng
f1873a014c Make tab prepend/append just edge cases of insert 2017-08-04 15:10:08 +01:00
Joe Cheng
48b8923b67 Properly escape jQuery selector strings 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
6f9f3fea83 implement navigation after hiding/removing selected tab 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
10f3320165 more JS code refactoring; improved documentation 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
d57aa33b40 insertion fully implemented 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
0e7c78bae3 refactored code and made insertion of navbarMenus possible 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
e6602786ec updated docs 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
31bbb3894c remove extra line 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
8bbf576807 typo: tag -> tab (makes a big difference!) 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
1ecc9b9d0e Fixed documentation problems and JS code logic 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
3adbebc3d9 document similar things together; add prependTab and appendTab 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
a4c086f51b now working for navbarPage and navlistPanel 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
0ecdcec698 clean up JS code (1 line only) 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
ae7f026d46 added NEWS and fixed typo 2017-08-04 15:10:08 +01:00
Barbara Borges Ribeiro
2813e0b706 update examples 2017-08-04 15:09:00 +01:00
Barbara Borges Ribeiro
a409562d00 delete extra brackets 2017-08-04 15:09:00 +01:00
Barbara Borges Ribeiro
b6b6661ea1 implement showTab and removeTab 2017-08-04 15:09:00 +01:00
Barbara Borges Ribeiro
fb7b6f667c implement removeTab 2017-08-04 15:09:00 +01:00
Barbara Borges Ribeiro
b94efe81e4 finish insertTab 2017-08-04 15:09:00 +01:00
Barbara Borges Ribeiro
72a1b3d2a0 add functions to index.r 2017-08-04 15:09:00 +01:00
Barbara Borges Ribeiro
20bff18bd4 changes 2017-08-04 15:09:00 +01:00
Winston Chang
ba5c5ef4fb Move isRunning function to better location 2017-07-27 14:56:01 -05:00
Barbara Borges Ribeiro
aff3ac0bb3 Add onStop function (#1770)
* NEWS item

* added `onStop` arg to `shinyApp()` (and renamed our internal `onEnd` - which is what was calling `on.exit()` already - to `onStop` as well)

* added onStop() function

* add entry for documentation

* make it work for all possible app structures (interactive, saved as app.R, saved as ui.R and server.R)

* fix #1772: make sure `onStart` works in all scenarios

* update NEWS

* improved wording

* more wording

* and more wording

* don't stop execution if a `onStop` callback function results in an error

* remove "(all sessions have been disconnected)" because it's misleading

* add @seealso documentation

* shamefully forgot to Cmd Shift D

* change code place

* Code review feedback

* onStop: use session argument instead of scope
2017-07-27 14:54:55 -05:00
Winston Chang
2c350daf01 Merge pull request #1802 from rstudio/bugfix/rstudio-debugger
work around RStudio debugger issue (closes #1474)
2017-07-27 13:14:01 -05:00
Winston Chang
cb7627c736 Update NEWS and add comment 2017-07-27 13:13:31 -05:00
Kevin Ushey
f731a5cae4 work around RStudio debugger issue (closes #1474) 2017-07-27 10:37:56 -07:00
Winston Chang
07cb7c9305 Add 'setSerializer' function (#1792) 2017-07-18 17:01:06 -05:00
Winston Chang
86e9cc4896 Add preprocessor for fileInputs that strips local path (#1789)
* Add preprocessor for fileInputs that strips local path

* Update NEWS

* Rename snapshotPreprocess to snapshotPreprocessOutput

* Add snapshotPreprocessInput function

* Remove unnecessary NEWS item

* Update NEWS

* Add getSnapshotPreprocessInput

* Add staticdocs entry for snapshotPreprocessInput

* Add private methods to get snapshotPreprocess functions

* Bump version to 1.0.3.9002
2017-07-13 16:07:16 -05:00
Joe Cheng
12c9405257 Merge pull request #1790 from rstudio/wch-warn-level
Don't reduce warn level when running app
2017-07-12 22:41:21 -07:00
Winston Chang
4708b44c59 Don't reduce warn level when running app. Fixes #1680 2017-07-12 19:29:49 -05:00
Winston Chang
4cb428bb92 Add a function to test if an app is running (#1785)
Squashed commit of the following:

commit 8667bed8962069a5cab8691f981e2b7ba9d449c3
Author: Winston Chang <winston@stdout.org>
Date:   Tue Jul 11 14:36:11 2017 -0500

    Edits

commit c4e8549ca5
Author: Konrad Rudolph <konrad.rudolph@gmail.com>
Date:   Fri Jul 7 17:57:33 2017 +0100

    Describe changes

commit 7b05c2e60f
Author: Konrad Rudolph <konrad.rudolph@gmail.com>
Date:   Fri Jul 7 17:54:40 2017 +0100

    Add new function to doc index

commit eb93ebfad8
Author: Konrad Rudolph <konrad.rudolph@gmail.com>
Date:   Fri Jul 7 17:54:30 2017 +0100

    Add documentatio for new function

commit 1a6c8a4d72
Author: Konrad Rudolph <konrad.rudolph@gmail.com>
Date:   Fri Jul 7 17:53:13 2017 +0100

    Add a function to test whether the app is running
2017-07-11 14:36:59 -05:00
Mine Cetinkaya-Rundel
d7391b19bc Convert examples to single file apps (#1685)
* - Convert all example apps to single file app.R file
- Make relevant updates to Readmes to match up with app.R structure
- Add color to plots (RStudio blue)
- In 04_mpg example: Show outliers by default, as opposed to hide, since this is more routine
- In 06_tabsets and 08_html examples: Don't name random data vector "data"
- Add extensive comments to app.R files and use consistent formatting of comments across examples
- In 09_upload example: Use req() to check for NULL entry

* add news entry summarizing changes

* use true RStudio blue, #75AADB

* Conver shinyApp calls at the end to drop argument name in examples 3-11, except for the custom HTML example. Kept them in for examples 1&2 for completeness in first exporuse to function.

* Pull news items that got added before this PR was merged

* Update comment for shinyApp function -- it creates an app object, doesn't run the app
2017-07-11 14:20:01 -05:00
Joe Cheng
db9e56d1ca Merge pull request #1768 from rstudio/wch-fix-with-private-seed
Fix withPrivateSeed
2017-07-11 12:17:02 -07:00
Winston Chang
e527af10f4 New version of httpuv is on CRAN 2017-07-11 13:45:45 -05:00
Joe Cheng
74c7be0a6d Merge pull request #1786 from rstudio/joe/bugfix/fileinput-content-type
Use a more suitable content type for file uploads
2017-07-10 15:39:13 -07:00
Joe Cheng
2d40e7b51a Use a more suitable content type for file uploads
application/x-www-form-urlencoded;charset=UTF-8 is the default, which shinyapps.io
cares about for some reason and tries to parse the data as such. By setting the
content type to the more accurate application/octet-stream, no middleware should
be tempted to futz with the contents.
2017-07-10 15:33:42 -07:00
Winston Chang
ea407fb2ea Don't include xtable comment in renderTable by default 2017-06-27 15:05:31 -05:00
Winston Chang
fca5b0529a Remove reinitalizeSeed
This function is no longer needed because the minimum R vesion supported by Shiny is 3.0.2.
2017-06-27 10:30:50 -05:00
Winston Chang
65fd1dd2d8 Remove branch name for httpuv remote 2017-06-26 22:40:37 -05:00
Winston Chang
0a7ede3818 Add tests for random streams 2017-06-26 21:59:52 -05:00
Winston Chang
24e84f3866 Prevent private random stream from leaking out. Fixes #1763 2017-06-26 21:59:51 -05:00
Winston Chang
c1c8e46c09 Refactor withPrivateSeed 2017-06-26 21:59:51 -05:00
Winston Chang
8591e4f301 Add working app for conditionalPanel example 2017-06-23 10:14:32 -05:00
Alan Dipert
10db7ad89c Support modules in conditionalPanel (#1735) 2017-06-23 10:12:15 -05:00
Joe Cheng
4ca4f442b9 Required R version is 3.0.2 due to sourcetools 2017-06-22 19:42:49 -05:00
Winston Chang
6d5ecbc9c4 Fix indentation 2017-06-22 13:18:05 -05:00
Winston Chang
ea685a5686 Don't send local package path to client when using htmlwidgets (#1756)
* Don't send local package path to client when using htmlwidgets. Fixes #1755

* Add scrubFile option
2017-06-22 13:16:19 -05:00
Winston Chang
376d3b6e91 Merge pull request #1760 from rstudio/wch-snapshot-preprocess
Add snapshotPreprocess function
2017-06-22 13:01:09 -05:00
Winston Chang
df7397af1f Bump version 2017-06-21 14:27:03 -05:00
Winston Chang
9ba9345b04 Add snapshotPreprocess function 2017-06-21 14:27:03 -05:00
Barbara Borges Ribeiro
9fc5758ae0 Triggers a new shiny:outputinvalidated event (#1758)
* trigger a new `shiny:invalidated` event when an output gets invalidated, at the same time that the `recalculating` CSS class is added (fixes #1688)

* add attribution to @andrewsali

* change event name from 'shiny:invalidated' to 'shiny:outputinvalidated'

* add binding and name to the new event 'shiny:outputinvalidated'
2017-06-21 12:28:51 -05:00
Winston Chang
25298a6182 In test mode, send message to client even when no outputs change (#1747)
* In test mode, send message to client even when no outputs change

* Update NEWS.md
2017-06-20 13:50:28 -05:00
Winston Chang
246da1bff6 Grunt 2017-06-16 13:48:38 -05:00
Carl Ganz
8b5d12b958 Add placeholder parameter to updateTextInput (#1742)
* add placeholder parameter

* add js placeholder code

* roxygenize

* grunt

* fix updateCheckBoxInput not to use placeholder

* simply roxygen

* add NEWS

* revert grunt
2017-06-15 22:00:39 -05:00
Alan Dipert
3817370d4e fileInput JS: Allow uploading the same file. (#1719)
* tools README: notes about entr + grunt

* fileInput JS: Allow uploading the same file. Fixes #1508

* Grunt

* Added note to NEWS.

* tools README: add Linux section, fix formatting
2017-06-15 15:09:37 -05:00
Alan Dipert
c29846a9da fileInput: Preserve extension of files uploaded from IE9 (#1717)
* fileInput: IE addendum to #1706

- Attempt to preserve the extension of files uploaded from IE9.

* maybeMoveIEUpload: Fix if spacing
2017-06-15 13:47:21 -05:00
Winston Chang
2158f906a7 Merge pull request #1736 from rstudio/wch-fix-unbind
Fix condition for calling exports.unbindAll()
2017-06-15 13:05:16 -05:00
Winston Chang
008dd280d6 Grunt 2017-06-08 17:03:21 -07:00
Winston Chang
fb99db011c Fix condition for calling exports.unbindAll(). (Correction to #1449) 2017-06-08 17:02:56 -07:00
Joe Cheng
c0fbd9cb3c Merge pull request #1732 from rstudio/barbara/mods
Fixed #1546: make it possible to write into a module's session$userData non-hackily
2017-06-07 22:45:47 -07:00
Barbara Borges Ribeiro
fb79b18002 More descriptive NEWS item and added an explanatory comment to the code 2017-06-07 13:28:51 -07:00
Barbara Borges Ribeiro
3841f22108 Fixed #1546: make it possible (without any hacks) to write arbitrary data into a module's session$userData 2017-06-07 12:11:05 -07:00
Winston Chang
379d523ac5 Add better error messages for errors parsing and evaluating JS code (#1727)
* Add better error messages for errors parsing and evaluating JS code

* Grunt
2017-06-02 14:31:06 -05:00
Winston Chang
07ec7f8c13 Update Rproj for new version of RStudio IDE 2017-06-02 13:31:33 -05:00
Alan Dipert
d0f29cc7a2 fileInput: If possible, retain uploaded file extensions on the server. (#1706) 2017-05-26 11:16:02 -05:00
Joe Cheng
0e23a487f7 Merge pull request #1713 from rstudio/jjallaire-contributing-links
Update links in CONTRIBUTING.md
2017-05-23 14:05:43 -07:00
JJ Allaire
ac10f7c426 Update links in CONTRIBUTING.md
Update the links to contributor agreements to reflect new versions that use my current email rather than rstudio.org based one.

I've made the same change in the rstudio and rmarkdown repos, we should make it in other repos that have a contributor agreement as well.
2017-05-23 16:38:02 -04:00
Joe Cheng
852c00009e Merge pull request #1712 from rstudio/wch-fix-reactiveval
Give each ReactiveVal separate dependents
2017-05-22 11:15:35 -07:00
Winston Chang
b365798e66 Add tests for ReactiveVal independence 2017-05-22 10:35:01 -05:00
Winston Chang
66a6097a49 Give each ReactiveVal separate dependents. Fixes #1710 2017-05-22 10:34:27 -05:00
Winston Chang
0e529d3d92 Fix partial arg match. Closes #1701 2017-05-10 10:08:05 -05:00
Winston Chang
06c75dd656 Bump version to 1.0.3.9000 2017-04-28 09:45:17 -05:00
Winston Chang
69c32d4d90 Bump version to 1.0.3 2017-04-25 15:33:10 -05:00
Winston Chang
36ffebd975 Workaround for NOTE about objects in yet-unreleased version of ggplot2 2017-04-25 15:33:10 -05:00
Winston Chang
deb56539fb Better reactivePoll example. Closes #1678 2017-04-25 10:48:29 -05:00
Winston Chang
af8d099b9f Don't call body(NULL). Fixes #1676 2017-04-24 13:42:22 -05:00
Winston Chang
eed869d321 Make fileInput progress bar change color on error (#1673)
* Make fileInput progress bar change color on error. Fixes #1672

* Grunt

* Update NEWS
2017-04-21 11:33:14 -05:00
Winston Chang
f8f2acf6c3 Bump version to 1.0.2.9000 2017-04-18 16:38:14 -05:00
Winston Chang
7be9f74827 Merge tag 'v1.0.2'
Shiny 1.0.2 on CRAN
2017-04-18 16:36:44 -05:00
Barbara Borges Ribeiro
ed77982330 Merge pull request #1670 from rstudio/joe/prebuilt
pre-built => prebuilt
2017-04-18 19:58:32 +01:00
Joe Cheng
e1b47eca90 pre-built => prebuilt 2017-04-18 11:09:45 -07:00
Winston Chang
bfa0b2d2bc Bump version to 1.0.2 and update NEWS 2017-04-13 14:13:41 -05:00
Winston Chang
d67783edbd Fix typo 2017-04-13 14:11:15 -05:00
Joe Cheng
77712b6664 Use RStudio replacement for deprecated MathJax CDN (#1664)
* Use RStudio replacement for deprecated MathJax CDN

* Add link to PR
2017-04-12 14:41:21 -05:00
Winston Chang
1633e7faa6 Fix Bootstrap URL. Closes #1662 2017-04-10 10:01:42 -05:00
Joe Cheng
2dc5ee5862 Merge pull request #1661 from rstudio/joe/bugfix/showcase-code-margin
Fix #1654: Empty space below showcase code
2017-04-07 17:39:01 -07:00
Joe Cheng
bbaea23eea Fix #1654: Empty space below showcase code 2017-04-07 17:37:54 -07:00
Barbara Borges Ribeiro
d112ac7eef fix documentation (worng/misleading code example) 2017-04-05 17:58:39 -05:00
Barbara Borges Ribeiro
cf21e987f2 Add shiny:sessionInit event (#1568)
* added a shiny:sessionInit JS event that is triggered at the end of the session's initialize method

* new entry

* update NEWS

* correct version number in NEWS.md

* fix typo
2017-04-05 10:50:42 -05:00
Barbara Borges Ribeiro
dae11765bc allow the choices argument in checkboxGroupInput() to be NULL (#1652)
* allow the `choices` argument in `checkboxGroupInput()` to be `NULL` or `c()` to keep backward compatibility with Shiny < 1.0.1 (fixes #1649)

* use vapply

* added one more test; reimplemented logic for checking if choice args are null
2017-04-05 10:12:44 -05:00
Barbara Borges Ribeiro
df30a3c7f4 PR: Add CC-BY-SA-4.0 license to showcase 2017-04-03 18:24:28 +01:00
Joe Cheng
aaa4600597 Bump version number to 1.0.1.9000 2017-04-03 10:22:08 -07:00
Joe Cheng
ba1730d26b Add CC-BY-SA-4.0 license to showcase 2017-04-03 10:18:04 -07:00
Barbara Borges Ribeiro
d1b5c812f7 re-run grunt to update version number embedded in shiny.min.js 2017-03-31 16:49:11 +01:00
Barbara Borges Ribeiro
5bfe6d1c84 bump version numbers in DESCRIPTION and NEWS 2017-03-31 16:48:13 +01:00
Winston Chang
9804a794fd Merge pull request #1641 from rstudio/fix-plot-rounding
Round brush coordinates
2017-03-31 10:46:44 -05:00
Winston Chang
0344645208 Grunt 2017-03-31 10:42:44 -05:00
Winston Chang
f56ad6e787 Update NEWS 2017-03-31 10:42:43 -05:00
Winston Chang
7492db592b Round brush coordinates to 14 digits. Fixes #1634 2017-03-31 10:42:43 -05:00
Winston Chang
2e80ecf8a7 Rebuild ion.rangeSlider.min.js 2017-03-30 15:48:47 -05:00
Joe Cheng
6993551a44 Fix #1637: Outputs stay faded on MS Edge (#1640) 2017-03-30 20:25:27 +01:00
Joe Cheng
9bff15adfe Fix #1632: Showcase mode comes up almost blank in IE9 & 11 (#1633)
If the width is made very wide in showcase mode with side-by-side
arrangement, the app shrinks to almost nothing. For some reason the
zoom CSS property (which we set using jQuery.animate) is set to
"1%" instead of "1".

Numbers and percentages are equally valid here, and the issue goes
away if we use percentage.
2017-03-29 10:16:17 -05:00
Winston Chang
0c24da2358 Stop propagation of mouse events on slider (#1631)
see (https://github.com/rstudio/shiny/issues/711)
2017-03-28 20:38:46 +01:00
Barbara Borges Ribeiro
4ee4adb43d doc changes 2017-03-27 21:12:46 +01:00
Joe Cheng
e33b028348 Merge pull request #1628 from rstudio/wch-rm-object-assign
Remove babel-polyfill
2017-03-27 13:07:47 -07:00
Winston Chang
384d9c1841 Grunt 2017-03-27 15:02:16 -05:00
Winston Chang
f78fcd6b5f Remove need for babel-polyfill 2017-03-27 15:02:03 -05:00
Barbara Borges Ribeiro
711a72989b Update NEWS.md 2017-03-27 17:53:59 +01:00
Barbara Borges Ribeiro
d62a2fc1d5 Allow arbitrary UI code in the choiceNames for radio buttons and checkbox group input (#1521) 2017-03-27 16:51:44 +01:00
Barbara Borges Ribeiro
f33f712a3a fix typo 2017-03-27 10:00:47 +01:00
Joe Cheng
3315b3310b Merge pull request #1614 from rstudio/joe/feature/reactiveVal
Add reactiveVal() for single reactive value
2017-03-24 13:08:47 -07:00
Joe Cheng
c7134b16ed Add link to PR for reactiveVal feature 2017-03-24 13:08:09 -07:00
Winston Chang
f36f710661 Make sure reactiveTimer gets session at creation time. Fixes #1621 2017-03-24 13:47:25 -05:00
Joe Cheng
00ab8681c7 Merge pull request #1619 from rstudio/wch-fileinput
Make fileInput text customizable. Closes #1617
2017-03-23 17:16:32 -07:00
Winston Chang
4137bbac94 NEWS 2017-03-23 14:33:22 -05:00
Winston Chang
750b2ad599 Make fileInput text customizable. Closes #1617 2017-03-23 14:31:31 -05:00
Joe Cheng
511c833fbb More code review feedback 2017-03-23 10:29:29 -07:00
Joe Cheng
29063a0c07 Code review feedback 2017-03-23 10:24:06 -07:00
Barbara Borges Ribeiro
67909b3557 updated tools/README.md (#1616) 2017-03-23 10:07:41 -05:00
Joe Cheng
102c12d36c Add NEWS item 2017-03-22 16:12:07 -07:00
Joe Cheng
dc51651665 Add S3 generics for format/print; freezeReactiveVal
Also changed the classes of reactive expressions and reactiveVal
from "reactive" and "reactiveVal" to c("reactiveExpr", "reactive")
and c("reactiveVal", "reactive")
2017-03-22 11:29:22 -07:00
Joe Cheng
8b563d6d5f Fix regex for old versions of R 2017-03-22 10:37:16 -07:00
Joe Cheng
eb8b88027e Automatic labelling of reactiveVals 2017-03-22 09:47:08 -07:00
Joe Cheng
a5b7f307ed Add reactiveVal() for single reactive value 2017-03-21 16:38:32 -07:00
Winston Chang
45fca425aa Change NS() to return a vectorized function (#1613)
* Change NS() to return a vectorized function

* Update NEWS

* Use vectorized ns()

* Use correct separator
2017-03-21 15:57:38 -05:00
Winston Chang
a0bd9b5fd7 Redocument with Roxygen 6.0.1 2017-03-21 14:02:29 -05:00
Winston Chang
c12e24e3e3 Properly register bookmark excludes for modules. Fixes #1598 (#1599)
* Scopes: properly register bookmark excludes. Fixes #1598

* Update NEWS
2017-03-21 13:56:08 -05:00
Winston Chang
d147c5a153 Don't use data-drag-interval for non-range sliders. Fixes #1605 (#1610)
* Don't use data-drag-interval for non-range sliders. Fixes #1605

* Update NEWS
2017-03-16 15:49:46 -05:00
Winston Chang
7a833456d9 Use consistent value caching format 2017-03-10 12:03:24 -06:00
Winston Chang
306f33dfc4 Fix value access 2017-03-09 16:16:16 -06:00
Winston Chang
a2745a4060 Grunt 2017-03-03 15:28:47 -06:00
Winston Chang
46b68c7b2a Bump version to 1.0.0.9001 2017-03-03 15:28:47 -06:00
Winston Chang
4264760113 Add binding and el fields to shiny:inputchanged event (#1596)
* Remove unused 'immediate' arguments

* Add opts argument to setInput methods

* Extract input values without opts

* Consistent interface for setting initial values

* Update NEWS

* Add binding and el when fileInputBinding triggers shiny:inputchanged

* Revert "Consistent interface for setting initial values"

This reverts commit 12c0b6e72a.

* Move InputDeferDecorater function

The new placement properly reflects the decorator stack

* Fix indentation

* bindInputs: make sure value is set immediately

* Only use opts where necessary in input decorators

* Properly send initial values

* Move initial value of .clientdata_allowDataUriScheme to better place

* Fix indentation

* Add InputValidateDecorator

* Better variable name

* Add function for default input options

* Simplify code
2017-03-03 15:27:32 -06:00
Winston Chang
42dedbbd9a Simplify user value check 2017-03-02 13:30:47 -06:00
Winston Chang
ea99bfdb16 Update NEWS 2017-02-28 10:48:48 -06:00
Winston Chang
2ccb934338 Merge pull request #1592 from akersting/master
fix: dateRangeInput did not respect weekstart arg
2017-02-28 10:46:35 -06:00
Winston Chang
367027cfbc Merge branch 'wch/redundant-setinput' 2017-02-28 09:23:17 -06:00
Winston Chang
c4ebd3b6d5 Merge pull request #1594 from rstudio/wch/fix-dynamic-input
Make sure input deduplication respects inputType. Closes #162
2017-02-28 09:19:41 -06:00
Winston Chang
5f8cd82a09 Update NEWS 2017-02-24 19:43:55 -06:00
Winston Chang
0ef15fa662 Remove redundant calls to setInput 2017-02-24 19:43:55 -06:00
Winston Chang
c05452af91 Update NEWS 2017-02-24 15:16:12 -06:00
Winston Chang
4c8bafcf9a Make sure input deduplication respects inputType. Closes #162 2017-02-24 15:11:18 -06:00
Andreas Kersting
034f30a49a fix: dateRangeInput did not respect weekstart arg 2017-02-23 07:38:10 +01:00
Winston Chang
0f13075e17 NEWS edits 2017-02-10 14:28:15 -06:00
Winston Chang
ad274a5981 Grunt 2017-02-10 14:26:48 -06:00
Winston Chang
fdbcbaec8a Merge pull request #1579 from albertosantini/fix-1577
Improve escapeHTML
2017-02-10 14:26:22 -06:00
Alberto Santini
9c09072ee6 Update NEWS 2017-02-10 20:16:57 +01:00
Alberto Santini
0a4ca56da9 Improve escapeHTML
Replacing one char after another is not a best practice, due to the order dependency of replacing, xss risk and performance.

Fix #1577
2017-02-10 18:15:07 +01:00
Winston Chang
2b494398f2 Merge pull request #1578 from rstudio/wch/ggplot-api
Add plot interaction support for ggplot2 api
2017-02-10 10:56:59 -06:00
Winston Chang
95585c2264 Update NEWS 2017-02-10 10:42:27 -06:00
Winston Chang
92f9f0da9e Restructure code for clarity 2017-02-09 11:23:13 -06:00
Winston Chang
fe943b5e95 Update plot interaction for ggplot2 > 2.2.1 2017-02-09 11:01:47 -06:00
Winston Chang
3479a4661a Prepare code for ggplot2 api 2017-02-02 11:12:22 -06:00
Winston Chang
7ba438cf7c Add entries to staticdocs index 2017-02-01 11:41:18 -06:00
Winston Chang
c761e9fba0 NEWS formatting fixes 2017-02-01 11:37:31 -06:00
Winston Chang
deae31ea4a Merge pull request #1570 from rstudio/wch/simplify-fileupload
Remove shiny:fileuploaded JS event
2017-02-01 11:34:01 -06:00
Winston Chang
547355a163 Grunt 2017-02-01 11:29:21 -06:00
Winston Chang
9be4cb132c NEWS 2017-02-01 11:29:21 -06:00
Winston Chang
3e25c9f3f4 Remove shiny:fileuploaded event 2017-02-01 11:17:08 -06:00
Barbara Borges Ribeiro
220c7e9139 decode URLs in staticHandler func - fixes #1565 (via #1566) 2017-02-01 06:27:11 +00:00
Winston Chang
79a085a9be Merge pull request #1547 from rstudio/wch/fix-progress
Fix progress bar
2017-01-31 20:38:38 -06:00
Winston Chang
b505c5a9d3 Grunt 2017-01-31 20:38:16 -06:00
Winston Chang
03ba660ea1 Update NEWS 2017-01-31 20:37:39 -06:00
Winston Chang
5aeb361f6d Set starting value to NULL 2017-01-31 20:36:28 -06:00
Winston Chang
0e519a4e97 Progress: store value as raw value instead of normalized 2017-01-31 20:36:28 -06:00
Winston Chang
4feee00d34 NULL value no longer makes progress bar go to 100%. Closes #1472
This also removes the documentation which said that using NULL would cause the
progress bar to be hidden.
2017-01-31 20:36:28 -06:00
Winston Chang
ef5e4cdc0a Merge pull request #1559 from rstudio/wch/download-event
Add shiny:filedownload Javascript event
2017-01-31 20:29:25 -06:00
Winston Chang
67c599f50b Update NEWS 2017-01-31 20:27:20 -06:00
Winston Chang
5af9b61357 Grunt 2017-01-31 20:25:00 -06:00
Winston Chang
1d6771b4ed Unexport markOutputAttrs and add snapshotExclude function 2017-01-31 20:24:52 -06:00
Winston Chang
c55dc0a58e Add ability to exclude outputs from snapshots 2017-01-31 20:23:34 -06:00
Winston Chang
c525d55db8 Add shiny:filedownload JS event 2017-01-31 20:23:34 -06:00
Winston Chang
408f66ef80 Merge pull request #1541 from rstudio/wch/file-input-event
Make fileInput trigger shiny:inputchanged.
2017-01-31 16:29:44 -06:00
Winston Chang
7f73a047a4 Grunt 2017-01-31 16:27:50 -06:00
Winston Chang
015bc98d60 Trigger inputchanged event when fileupload is completed 2017-01-31 16:27:16 -06:00
Winston Chang
5cd9ba609a Make fileInput trigger shiny:inputchanged. Closes #1511 2017-01-31 16:27:16 -06:00
Winston Chang
c8ed6544db Fix documentation link. Closes #1567 2017-01-31 11:40:24 -06:00
Winston Chang
1162113d3b Re-document 2017-01-30 13:47:20 -06:00
Winston Chang
1612503e7b Update selectize URLs. Closes #1564 2017-01-30 13:46:12 -06:00
Joe Cheng
34ba85df3b Merge pull request #1563 from rstudio/barbara/userInfo
Barbara/user info
2017-01-30 09:55:56 -08:00
Barbara Borges Ribeiro
8206e7d2a2 delete old message handler 2017-01-27 20:42:04 +00:00
Barbara Borges Ribeiro
3e29672c70 news item 2017-01-27 20:40:07 +00:00
Barbara Borges Ribeiro
f67aaafe4f some adjustments after feedback 2017-01-27 20:39:18 +00:00
Barbara Borges Ribeiro
ed704afc07 remove console.log and re-grunt 2017-01-27 20:38:17 +00:00
Barbara Borges Ribeiro
bbbfacb4b2 grunt 2017-01-27 20:37:58 +00:00
Barbara Borges Ribeiro
cf16d2e52d listify 2017-01-27 20:37:32 +00:00
Barbara Borges Ribeiro
6268e6e1c9 will be removed!! only for testing purposes 2017-01-27 20:36:37 +00:00
Barbara Borges Ribeiro
99b8e5b303 stuff 2017-01-27 20:36:37 +00:00
Winston Chang
73446af330 Convert tabs to spaces in examples 2017-01-26 15:06:15 -06:00
Barbara Borges Ribeiro
a0b917a207 support pushState for pseudo-nav
see the documentation for details (`?updateQueryString` and `?getQueryString`)
2017-01-25 23:45:26 +00:00
Winston Chang
53ec7edd06 Another typo 2017-01-17 12:02:39 -05:00
Winston Chang
ff804c0ff8 Typo 2017-01-17 12:01:51 -05:00
Winston Chang
9d69ff01b3 Update ion.rangeSlider to 2.1.6 (#1540)
* Update ion.rangeSlider to 2.1.6

* Simplify code when slider separator is ""

* Add links to NEWS
2017-01-17 12:00:47 -05:00
Winston Chang
61831f530f Merge pull request #1525 from rstudio/wch/ggplot-coord-fixed
Add plot interaction support for coord_fixed
2017-01-16 13:01:01 -05:00
Winston Chang
6065db1d24 Update NEWS 2017-01-16 13:00:41 -05:00
Winston Chang
270b8415e8 Add plot interaction support for coord_fixed. Closes #1121 2017-01-16 12:58:58 -05:00
Winston Chang
1987331a70 Bump version to 1.0.0.9000 2017-01-16 12:55:36 -05:00
Winston Chang
ab85216b96 Merge tag 'v1.0.0' 2017-01-13 13:36:23 -05:00
Winston Chang
b5cb78c77e Update URL 2017-01-10 10:13:06 -06:00
Winston Chang
e75c99672d Update NEWS 2017-01-09 12:38:48 -06:00
Winston Chang
7faba72ebe Fix URL 2017-01-09 12:38:48 -06:00
Winston Chang
cbe8fc1bdf Bump version to 1.0.0 2017-01-09 12:38:48 -06:00
Winston Chang
f66a7660e2 Merge pull request #1529 from rstudio/feature/res-path-numeric-prefix
Relax naming requirements for addResourcePath
2017-01-09 12:28:07 -06:00
Winston Chang
5f3159a203 Add link to PR in NEWS 2017-01-09 12:25:32 -06:00
JJ Allaire
76aeda4436 refine regex 2017-01-09 12:32:12 -05:00
JJ Allaire
fa791cd28c Relax naming requirements for addResourcePath()
First character no longer needs to be a letter. See https://github.com/rstudio/tutor/issues/4 for discussion.
2017-01-09 11:04:51 -05:00
Winston Chang
d836c68ee5 Grunt 2017-01-03 16:17:48 -06:00
Winston Chang
519d90f0a7 Update NEWS 2017-01-03 16:17:28 -06:00
Winston Chang
26400be6f7 Pressing Esc in a modal in a gadget only closes the modal. Closes #1453 (#1523) 2017-01-03 17:14:31 -05:00
Winston Chang
92ba7e9d54 Update yarn install instructions 2017-01-03 14:29:43 -06:00
Winston Chang
25eafe1e69 NEWS: more info on testing 2017-01-03 14:16:55 -06:00
Winston Chang
118a9ca861 Update NEWS 2017-01-03 12:54:06 -06:00
Winston Chang
174a1fe834 Update to font-awesome 4.7.0 2017-01-03 12:47:23 -06:00
Winston Chang
1e0f3f40a9 Replace structure(NULL) with structure(list())
In R-devel 71841, structure(NULL) was deprecated.
2016-12-28 16:43:29 -06:00
Barbara Borges Ribeiro
19623694f5 Added skipFirst arg to observeEvent (#1494)
* added skipFirs arg to observeEvent

* create getCurrentObserver() function

* better NEWS entry

* made code more consistent

* implemented `once` param to `observeEvent`; extensive documentation for `getCurrentObserver`

* implement dig param to `getCurrentObserver`

* fix bug that was causing unit tests to fail

* take two

* git commit

* removed function getCurrentObserver

* delete .globals$currentObserver variable

* update docs

* typo

* remove dupes in index.r (bah humbug)

* rerun devtools::document
2016-12-19 15:51:19 -08:00
Winston Chang
55a16043e1 Merge pull request #1510 from rstudio/joe/feature/debounce
Add reactive debounce and throttle functions
2016-12-16 11:10:02 -06:00
Winston Chang
29943b7edd Merge pull request #1482 from rstudio/barbara/runapp
Fixes #1358: more informative error message when calling runApp inside of an app's app.R
2016-12-16 10:15:35 -06:00
Joe Cheng
a1e2af9533 Add debounce/throttle tests, priority arg 2016-12-15 14:52:07 -08:00
Barbara Borges Ribeiro
c350e2a668 Fixes #1358: more informative error message when calling runApp inside of an app's app.R (or inside ui.R or server.R). 2016-12-15 21:50:39 +00:00
Joe Cheng
e0868ba2ab Fix #1013: flushReact should be called after app loads (#1503)
* Fix #1013: flushReact should be called after app loads

* Add link to pull request
2016-12-15 13:16:18 -06:00
Joe Cheng
bcefd1fbd8 Fix #117: Reactive expressions hold on to memory for longer than necessary (#1504)
* Fix #117: Reactive expressions hold on to memory for longer than necessary

* Fix broken link

* Add link to pull request
2016-12-15 13:15:00 -06:00
Joe Cheng
f5fbad0abf Add link to pull request 2016-12-15 11:14:48 -08:00
Joe Cheng
95b1a197be Remove unnecessary namespace 2016-12-15 11:11:29 -08:00
Joe Cheng
39169a36f5 Wording tweaks 2016-12-15 11:10:28 -08:00
Joe Cheng
3b1a409f07 Remove unnecessary link qualifier 2016-12-15 11:01:35 -08:00
Joe Cheng
accd70d4b4 Add session$userData feature (#1513)
* Add session$userData

* Tweak wording of NEWS

* Fix broken links
2016-12-15 12:50:20 -06:00
Winston Chang
3c7f4b760f Merge pull request #1514 from rstudio/joe/bugfix/fix-broken-links
Fix unqualified links to other packages
2016-12-15 12:42:19 -06:00
Joe Cheng
f7d7ccfd2c Fix unqualified links to other packages
R-devel warns on this now, causes Travis to fail
2016-12-15 10:35:46 -08:00
Joe Cheng
de98a03887 Add limitations section to debounce/throttle docs 2016-12-13 17:48:36 -08:00
Joe Cheng
0e11c240cb Add magrittr as Suggests because of ?debounce example 2016-12-13 17:29:29 -08:00
Joe Cheng
c0a298e484 Add reactive debounce and throttle functions 2016-12-13 17:22:12 -08:00
Winston Chang
907b9a9862 Merge pull request #1480 from rstudio/barbara/verbatim
Closes #1357: verbatimTextOutput should optionally be hidden if no content
2016-12-07 10:13:04 -06:00
Barbara Borges Ribeiro
8d70d91cf4 fix #1487: better error handling for insertUI when selector does not match anything in DOM (do console log) 2016-12-07 05:10:43 +00:00
Barbara Borges Ribeiro
6fb86859ce use past tense in all NEWS.md entries 2016-12-07 04:55:08 +00:00
Barbara Borges Ribeiro
fe733b319f Fixes #969: allow navbarPage's fluid param to control both containers 2016-12-07 00:22:33 +00:00
Barbara Borges Ribeiro
08b58f3055 allow navbarPage's fluid param to control both the content *and* the header containers 2016-12-07 00:21:49 +00:00
Barbara Borges Ribeiro
9f6659f526 added a new arg (placeholder = FALSE) to verbatimTextOutput() so that by default it doesn't show up when it is empty; improved the documentation example 2016-12-07 00:02:40 +00:00
Barbara Borges Ribeiro
d28397df93 Fix #1359: shinyApp options argument ignored when passed to runApp (#1483) 2016-12-06 20:52:19 +00:00
Barbara Borges Ribeiro
2e1c37146b Add ... arg to downloadButton (merge #1492)
Add ... arg to downloadButton
2016-12-05 15:37:02 +00:00
Barbara Borges Ribeiro
903adc8f97 Added ability to pass arguments to the a tag function called inside downloadButton() and downloadLink(). Closes #986. 2016-12-05 15:35:31 +00:00
Winston Chang
fc7f454382 Merge pull request #1449 from rstudio/barbara/bugfix/insert-ui
Fix #1438: `unbindAll()` should not be called when inserting content with `insertUI()`
2016-12-01 15:45:23 -06:00
Winston Chang
ef35fc63a1 Grunt 2016-12-01 15:41:51 -06:00
Barbara Borges Ribeiro
52a193b183 unbindAll() should not be called when inserting content with insertUI() 2016-12-01 15:40:17 -06:00
Winston Chang
dad401a6ec Merge pull request #1464 from rstudio/testmode-inject-js
Add support for injecting JS code when in test mode
2016-12-01 14:52:12 -06:00
Winston Chang
ec3f8118db Grunt 2016-12-01 10:47:02 -06:00
Winston Chang
cfc0194c00 Sort input, output, export by name 2016-12-01 10:46:46 -06:00
Winston Chang
dd28f52301 Add sortByName function 2016-12-01 10:46:46 -06:00
Winston Chang
9dcbd532e6 Add getTestSnapshotBaseUrl function 2016-12-01 10:46:46 -06:00
Winston Chang
16b4a2cad2 Rename testEndpointUrl to testSnapshotUrl 2016-12-01 10:46:46 -06:00
Winston Chang
bd9d8a035a Change arguments from plural to singular 2016-12-01 10:46:46 -06:00
Winston Chang
d55ffb0212 Change default snapshot format to JSON 2016-12-01 10:46:46 -06:00
Winston Chang
e76ddfd005 Emit message when running in test mode 2016-12-01 10:46:46 -06:00
Winston Chang
59145a3b40 Add testmode as an option to runApp 2016-12-01 10:46:46 -06:00
Winston Chang
c993f5343b Bump version to 0.14.2.9001 2016-12-01 10:46:46 -06:00
Winston Chang
b62acec5ee Use singular form of input, export, and output 2016-12-01 10:46:46 -06:00
Winston Chang
b34ab9cdd5 Add shiny:fileuploaded JS event 2016-12-01 10:46:46 -06:00
Winston Chang
e0a8ab852e Update NEWS 2016-12-01 10:46:46 -06:00
Winston Chang
bd5ebd0e41 Remove token check 2016-12-01 10:46:46 -06:00
Winston Chang
661e21d25b Safer method for injecting code in test mode 2016-12-01 10:46:46 -06:00
Winston Chang
dc69a2bc94 Make sure test values are named vectors 2016-12-01 10:46:46 -06:00
Winston Chang
e6fec6b27d Rename variable 2016-12-01 10:46:46 -06:00
Winston Chang
27b92f9838 Add args to getTestEndpointUrl 2016-12-01 10:46:46 -06:00
Winston Chang
3446def4dd Basic code injection support 2016-12-01 10:46:46 -06:00
Barbara Borges Ribeiro
2700206715 Improve documentation for submitButton and change 07_widgets example to use an action button (#1475)
* update 07_widgets example

* improved documentation for submitButton (including a warnign section and an full-app example)

* typo

* update documentation based on Winton's feedback
2016-11-28 13:19:40 -06:00
Winston Chang
fdfc6f70f3 Merge branch 'barbara/contributing' 2016-11-22 14:56:25 -06:00
Winston Chang
065c288edb Edits to contribution guidelines 2016-11-22 14:56:12 -06:00
Barbara Borges Ribeiro
3121d2c23e mention support for the optgroup tag in the documention for selectInput (specifically in the choices arg). Added example app too. 2016-11-22 11:28:35 -06:00
Barbara Borges Ribeiro
7cd3bb524c add download attribute to the a tag that generates downloadButoon and downloadLink 2016-11-18 21:00:28 -08:00
Barbara Borges Ribeiro
6b8cc97779 drafted new contribution guidelines 2016-11-19 02:22:16 +00:00
Winston Chang
b7112a1edd Add link to NEWS 2016-11-10 15:26:46 -06:00
Jonathan
28965b7356 Render HTML dependency <meta> tag contents correctly (#1463)
* render HTML dependency <meta> tag contents correctly

* use direct address rather than loop; update NEWS
2016-11-10 15:10:18 -06:00
Dean Attali
bd3aa28416 fix typo in dateInput documentation (#1454) 2016-11-02 09:53:40 -05:00
Winston Chang
9fed4ce24c Bump version to 0.14.2.9000 2016-11-01 15:40:04 -05:00
Winston Chang
90383e30dd Bump version to 0.14.2 2016-10-31 10:19:40 -05:00
Winston Chang
13f184e957 Remove NEWS entry for change that was reverted 2016-10-31 10:17:51 -05:00
Winston Chang
a7a2c6d7ff Add list2env wrapper, for R <3.2.0 (#1446)
* Add list2env wrapper, for R <3.2.0

* Update NEWS
2016-10-28 13:56:52 -05:00
Winston Chang
d1bf39d0ac Add exportTestValues function (#1436)
* Add onTestSnapshot function

* Add shiny.testing option

* Add entry to staticdocs index

* Bump version to 0.14.1.9002 and update NEWS

* Document params for onTestSnapshot

* Add session$enableTestEndpoint() method

* Un-export applyInputHandlers

* Grunt

* Provide inputs, outputs, and snapshot at test endpoint

* Remove non-working example

* Fix var name in documentation

* Rename shiny.testing to shiny.testmode

* Rename onTestSnapshot to exportTestValues and add example

* Add session$getTestEndpointUrl

* Grunt

* Add module support to exportTestValues

* Test endpoint allows specifying specific values

* session$getTestEndpointUrl: add arguments for choosing which values to return
2016-10-27 21:08:34 -05:00
Joe Cheng
7dff6b8415 Merge pull request #1387 from sipemu/master
options render for updateSelectizeInput did not worked in modules
2016-10-27 11:11:16 -07:00
Dean Attali
656e019829 allow overriding a JS custom message handler; fixes #1419 (#1445)
* allow overriding existing custom JS message handlers

* when a JS handler gets re-defined, only use the most recent one

* JS handler overwrite: changes re winston's comments

* overwrite JS handler: add NEWS item

* fix wrong URL in NEWS
2016-10-27 13:07:34 -05:00
Winston Chang
2133b0f498 Use === in Javascript 2016-10-26 21:01:36 -05:00
Winston Chang
bc4dcee2b1 allow shiny.trace option to specify which type of messages to relay; fixes #1422 (#1428)
Squashed commit of the following:

commit bdc4080032ff6b5b2de0f799aa307272f3905003
Author: Dean Attali <daattali@gmail.com>
Date:   Mon Oct 17 18:18:03 2016 -0700

    add PR link to news item

commit 22c695cde2b270ba8ec37d4862ad1f30de76ce68
Author: Dean Attali <daattali@gmail.com>
Date:   Mon Oct 17 15:01:24 2016 -0700

    update NEWS for #1422 fix

commit e669548c13f84f0929e4131c641a8333e08baa26
Author: Dean Attali <daattali@gmail.com>
Date:   Sat Oct 15 12:45:49 2016 -0700

    allow shiny.trace option to specify which type of messages to relay; fixes #1422
2016-10-26 12:24:00 -05:00
Winston Chang
0e8cf95739 Pass shinysession to applyInputHandlers
This fixes a problem where input handlers that require a session object
would throw errors.
2016-10-25 10:27:03 -05:00
Joe Cheng
e133290c57 Fix #1399: Duplicate binding error with insertUI and nested uiOutput (#1402)
* Fix #1399: Duplicate binding error with insertUI and nested uiOutput

* Update NEWS.md
2016-10-18 20:22:02 -05:00
shrektan
1429b0677e fix a typo: option() -> options() 2016-10-18 14:56:38 -05:00
Barbara Borges Ribeiro
d03ee36647 Fixes #1427: add event delegation so that modals do not close by mistake (#1430)
* Fixes #1427: add event delegation so that modal does not close when an element inside it is triggered as hidden

* use `this === e.target` instead

* added NEWS item

* `e.target` must be equal to `$(#shiny-modal)`, not `this`
2016-10-18 14:54:27 -05:00
Winston Chang
6e5880c642 Bump version and update NEWS 2016-10-18 13:51:43 -05:00
Winston Chang
fa93cffafb Add entry to staticdocs 2016-10-18 13:51:43 -05:00
Winston Chang
ce9af0fb57 Export function that applies input handlers 2016-10-18 13:51:43 -05:00
Winston Chang
95700d8d51 Fix dategrange comment 2016-10-17 13:37:25 -05:00
Winston Chang
fb15e98519 Merge pull request #1429 from rstudio/slider-setvalue
Make sliderInputBinding.setValue update value immediately
2016-10-17 12:29:14 -05:00
Winston Chang
3054cb7971 Update NEWS 2016-10-17 12:28:36 -05:00
Winston Chang
f84587cf5a Grunt 2016-10-17 12:22:21 -05:00
Winston Chang
538f38f314 sliderInputBinding: setValue changes value immediately 2016-10-17 12:22:21 -05:00
Winston Chang
06578349c7 Document InputBinding.subscribe's callback argument 2016-10-17 12:18:12 -05:00
Winston Chang
a807476171 sliderInputBinding: rename 'updating' to 'immediate' 2016-10-17 12:13:15 -05:00
Winston Chang
7aacf9ca89 Use Yarn instead of npm (#1416) 2016-10-12 12:51:05 -05:00
Winston Chang
50dae5fb83 Remove unneeded npm package 2016-10-11 13:04:38 -05:00
Winston Chang
0853c425fe Bump version to 0.14.1.9000 in DESCRIPTION 2016-10-11 12:59:06 -05:00
Barbara Borges Ribeiro
edcc676693 add "fade" arg to modalDialog() (#1414)
* add "fade" arg to modalDialog() that can be set to FALSE to remove default modal animation

* added documentation

* reflow comments

* news item
2016-10-10 15:03:25 -05:00
Winston Chang
c8a742a121 Bump version and update NEWS 2016-10-05 09:32:36 -05:00
Winston Chang
ee14a7e15f Merge tag 'v0.14.1'
Shiny 0.14.1 on CRAN
2016-10-05 09:29:07 -05:00
Winston Chang
e1eaccf409 Fix tests for compiled code on R-devel. Closes #1404 2016-10-03 16:23:11 -05:00
Winston Chang
d2aae52868 Update NEWS 2016-09-30 15:39:31 -05:00
Winston Chang
9158fb4745 Bump version to 0.14.1 2016-09-30 15:39:31 -05:00
Winston Chang
0ff5ef5337 Remove file 2016-09-30 15:39:13 -05:00
Joe Cheng
1ace145f85 Merge pull request #1392 from rstudio/ggplot-fix
Add plot interaction support for ggplot>2.1.0
2016-09-30 12:40:30 -07:00
Winston Chang
565eb4b450 Merge pull request #1397 from rstudio/barbara/bugfix/radio
Maintain names of factors when updating radio buttons' choices
2016-09-29 14:18:24 -05:00
Barbara Borges Ribeiro
f39861c43f more comments and NEWS 2016-09-29 19:48:56 +01:00
Barbara Borges Ribeiro
72838c248f news entry 2016-09-29 19:16:46 +01:00
Barbara Borges Ribeiro
9be8765ccf more tests 2016-09-29 19:14:18 +01:00
Barbara Borges Ribeiro
48732c4393 deleted commented out lines 2016-09-29 19:14:18 +01:00
Barbara Borges Ribeiro
5bf0b7c920 a better fix 2016-09-29 19:14:18 +01:00
Barbara Borges Ribeiro
51a4580d0f maintain names of factors when updating radio buttons' choices 2016-09-29 19:14:18 +01:00
Winston Chang
266e611afa Update NEWS 2016-09-27 23:07:23 -05:00
Winston Chang
22598b693c Add more plot interaction tests 2016-09-27 23:07:23 -05:00
Winston Chang
008fe38f10 Add support for coord_flip 2016-09-27 23:07:17 -05:00
Winston Chang
24e8123240 Add plot interaction support for ggplot>2.1.0 2016-09-27 16:02:28 -05:00
Simon Müller
6054f03c0d Update update-input.R 2016-09-21 22:53:41 +02:00
Winston Chang
476f6d83e2 Remove no-longer-necessary CSS class 2016-09-19 10:22:40 -05:00
Winston Chang
ec57109f39 Merge pull request #1374 from rstudio/datepicker-noconflict
Fix datepicker conflicts
2016-09-19 09:30:22 -05:00
Winston Chang
d73488f887 Grunt 2016-09-19 09:27:28 -05:00
Winston Chang
3201380c29 Set value after min in max when updating dates 2016-09-19 09:27:28 -05:00
Winston Chang
1f04b39ae3 Rename bootstrapDP to bsDatepicker 2016-09-16 23:39:26 -05:00
Winston Chang
9e2b47027c Update NEWS 2016-09-16 23:39:26 -05:00
Winston Chang
662149a98a Update to jQuery UI 1.12.1 2016-09-16 23:39:26 -05:00
Winston Chang
fafa31589d Update NEWS 2016-09-16 23:39:26 -05:00
Winston Chang
43a5940b9e Updates to dateRangeInputBinding for new datepicker API 2016-09-16 23:39:26 -05:00
Winston Chang
33908624fd Fix off-by-one error for datepicker's setStartDate and setEndDate 2016-09-16 23:39:26 -05:00
Winston Chang
ffef8a341f Add workaround for bootstrap datepicker bug with setStartDate and setEndDate 2016-09-16 23:39:26 -05:00
Winston Chang
a48c5df844 Don't try to set min/max date when undefined 2016-09-16 23:39:26 -05:00
Winston Chang
37b6a668ab Fix off-by-one dates 2016-09-16 23:39:26 -05:00
Winston Chang
2a9a7cc897 Enable noconflict for Bootstrap Datepicker. Closes #1346 2016-09-16 23:39:26 -05:00
Winston Chang
c62e6b5734 Update to Bootstrap Datepicker 1.6.4. Closes #1218 2016-09-16 23:39:26 -05:00
Winston Chang
6ec1d0b935 grunt clean && grunt 2016-09-16 23:37:31 -05:00
Winston Chang
6c5769fdd8 Add missing files to Grunt clean 2016-09-16 23:36:57 -05:00
Winston Chang
09acc5920c Allow using no separator for sliderInput numbers. Fixes #1369 2016-09-15 12:03:25 -05:00
Winston Chang
9613c58bf8 Merge pull request #1370 from rstudio/fix-bookmark-fileinput
Fix bookmark fileinput
2016-09-13 15:38:52 -05:00
Winston Chang
147f9ac64b Remove sourcetools workaround for Travis
This is no longer necessary because the new version of sourcetools on CRAN
does not need C++11
2016-09-13 12:52:15 -05:00
Winston Chang
cc1e8961a1 Update NEWS 2016-09-13 11:24:42 -05:00
Winston Chang
3b1b2f401d Use createUniqueId for consistency 2016-09-13 11:14:29 -05:00
Winston Chang
58a87b9b61 Copy restored file to temp directory 2016-09-13 11:13:21 -05:00
Winston Chang
f09475a6b5 Better check for unsafe paths in restored file inputs 2016-09-13 11:12:16 -05:00
Winston Chang
750422d858 Mark restored file inputs with correct serializer. Closes #1368 2016-09-13 10:49:08 -05:00
Winston Chang
03d911d335 Use cache for AppVeyor 2016-09-13 10:47:00 -05:00
Winston Chang
2747c11a46 Bump version to 0.14.0.9000 2016-09-13 10:46:15 -05:00
Winston Chang
5a9fe2637f Update shiny.rstudio.com URLs in NEWS 2016-09-12 09:36:32 -05:00
499 changed files with 46422 additions and 19222 deletions

View File

@@ -18,3 +18,5 @@
^.*\.o$
^appveyor\.yml$
^revdep$
^TODO-promises.md$
^manualtests$

View File

@@ -10,8 +10,3 @@ notifications:
email:
on_success: change
on_failure: change
# Set CXX1X for R-devel, as R-devel does not detect CXX1X support for gcc 4.6.3.
# This was causing dependency sourcetools to fail.
before_install:
- if [[ "$TRAVIS_R_VERSION_STRING" = 'devel' ]]; then mkdir ~/.R && echo 'CXX1X=g++ -std=c++0x -g -O2 -fPIC' > ~/.R/Makevars; fi

View File

@@ -1,10 +1,38 @@
We welcome contributions to the **shiny** package. To submit a contribution:
1. [Fork](https://github.com/rstudio/shiny/fork) the repository and make your changes.
2. Ensure that you have signed the [individual](http://www.rstudio.com/wp-content/uploads/2014/06/RStudioIndividualContributorAgreement.pdf) or [corporate](http://www.rstudio.com/wp-content/uploads/2014/06/RStudioCorporateContributorAgreement.pdf) contributor agreement as appropriate. You can send the signed copy to jj@rstudio.com.
2. Ensure that you have signed the [individual](https://rstudioblog.files.wordpress.com/2017/05/rstudio_individual_contributor_agreement.pdf) or [corporate](https://rstudioblog.files.wordpress.com/2017/05/rstudio_corporate_contributor_agreement.pdf) contributor agreement as appropriate. You can send the signed copy to jj@rstudio.com.
3. Submit a [pull request](https://help.github.com/articles/using-pull-requests).
We'll try to be as responsive as possible in reviewing and accepting pull requests. We appreciate your contributions!
We generally do not merge pull requests that update included web libraries (such as Bootstrap or jQuery) because it is difficult for us to verify that the update is done correctly; we prefer to update these libraries ourselves.
## How to make changes
Before you submit a pull request, please do the following:
* Add an entry to NEWS.md concisely describing what you changed.
* If appropriate, add unit tests in the tests/ directory.
* If you made any changes to the JavaScript files in the srcjs/ directory, make sure you build the output JavaScript files. See tools/README.md file for information on using the build system.
* Run Build->Check Package in the RStudio IDE, or `devtools::check()`, to make sure your change did not add any messages, warnings, or errors.
Doing these things will make it easier for the Shiny development team to evaluate your pull request. Even so, we may still decide to modify your code or even not merge it at all. Factors that may prevent us from merging the pull request include:
* breaking backward compatibility
* adding a feature that we do not consider relevant for Shiny
* is hard to understand
* is hard to maintain in the future
* is computationally expensive
* is not intuitive for people to use
We will try to be responsive and provide feedback in case we decide not to merge your pull request.
## Filing issues
If you find a bug in Shiny, you can also [file an issue](https://github.com/rstudio/shiny/issues/new). Please provide as much relevant information as you can, and include a minimal reproducible example if possible.

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 0.14
Version: 1.2.0
Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
@@ -14,7 +14,7 @@ Authors@R: c(
person(family = "jQuery contributors", role = c("ctb", "cph"),
comment = "jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt"),
person(family = "jQuery UI contributors", role = c("ctb", "cph"),
comment = "jQuery UI library; authors listed in inst/www/shared/jqueryui/1.10.4/AUTHORS.txt"),
comment = "jQuery UI library; authors listed in inst/www/shared/jqueryui/AUTHORS.txt"),
person("Mark", "Otto", role = "ctb",
comment = "Bootstrap library"),
person("Jacob", "Thornton", role = "ctb",
@@ -56,22 +56,28 @@ Authors@R: c(
)
Description: Makes it incredibly easy to build interactive web
applications with R. Automatic "reactive" binding between inputs and
outputs and extensive pre-built widgets make it possible to build
outputs and extensive prebuilt widgets make it possible to build
beautiful, responsive, and powerful applications with minimal effort.
License: GPL-3 | file LICENSE
Depends:
R (>= 3.0.0),
R (>= 3.0.2),
methods
Imports:
utils,
httpuv (>= 1.3.3),
grDevices,
httpuv (>= 1.4.4),
mime (>= 0.3),
jsonlite (>= 0.9.16),
xtable,
digest,
htmltools (>= 0.3.5),
R6 (>= 2.0),
sourcetools
sourcetools,
later (>= 0.7.2),
promises (>= 1.0.1),
tools,
crayon,
rlang
Suggests:
datasets,
Cairo (>= 1.5-5),
@@ -79,7 +85,8 @@ Suggests:
knitr (>= 1.6),
markdown,
rmarkdown,
ggplot2
ggplot2,
magrittr
URL: http://shiny.rstudio.com
BugReports: https://github.com/rstudio/shiny/issues
Collate:
@@ -88,15 +95,21 @@ Collate:
'stack.R'
'bookmark-state.R'
'bootstrap-layout.R'
'globals.R'
'conditions.R'
'map.R'
'globals.R'
'utils.R'
'bootstrap.R'
'cache.R'
'cache-context.R'
'cache-disk.R'
'cache-memory.R'
'cache-utils.R'
'diagnose.R'
'fileupload.R'
'graph.R'
'reactives.R'
'reactive-domains.R'
'history.R'
'hooks.R'
'html-deps.R'
'htmltools.R'
@@ -118,6 +131,7 @@ Collate:
'input-text.R'
'input-textarea.R'
'input-utils.R'
'insert-tab.R'
'insert-ui.R'
'jqueryui.R'
'middleware-shiny.R'
@@ -128,8 +142,7 @@ Collate:
'priorityqueue.R'
'progress.R'
'react.R'
'reactive-domains.R'
'reactives.R'
'render-cached-plot.R'
'render-plot.R'
'render-table.R'
'run-url.R'
@@ -141,7 +154,9 @@ Collate:
'shinyui.R'
'shinywrappers.R'
'showcase.R'
'snapshot.R'
'tar.R'
'test-export.R'
'timer.R'
'update-input.R'
RoxygenNote: 5.0.1
RoxygenNote: 6.1.0

324
LICENSE
View File

@@ -12,7 +12,7 @@ these components are included below):
- Respond.js, https://github.com/scottjehl/Respond
- bootstrap-datepicker, https://github.com/eternicode/bootstrap-datepicker
- Font Awesome, https://github.com/FortAwesome/Font-Awesome
- selectize.js, https://github.com/brianreavis/selectize.js
- selectize.js, https://github.com/selectize/selectize.js
- es5-shim, https://github.com/es-shims/es5-shim
- ion.rangeSlider, https://github.com/IonDen/ion.rangeSlider
- strftime for Javascript, https://github.com/samsonjs/strftime
@@ -673,7 +673,7 @@ bootstrap-datepicker
limitations under the License.
Font-Awesome (CSS file is MIT licensed; font has SIL Open Font License 1.1)
Font Awesome (CSS files are MIT licensed; fonts have SIL Open Font License 1.1, svgs have CC BY 4.0 License)
----------------------------------------------------------------------
The MIT License (MIT)
@@ -795,6 +795,326 @@ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
=======================================================================
Creative Commons Attribution 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution 4.0 International Public License ("Public License"). To the
extent this Public License may be interpreted as a contract, You are
granted the Licensed Rights in consideration of Your acceptance of
these terms and conditions, and the Licensor grants You such rights in
consideration of benefits the Licensor receives from making the
Licensed Material available under these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
d. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
e. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
f. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
g. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
h. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
i. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
j. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
k. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
4. If You Share Adapted Material You produce, the Adapter's
License You apply must not prevent recipients of the Adapted
Material from complying with this Public License.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
selectize.js
----------------------------------------------------------------------

View File

@@ -22,7 +22,10 @@ S3method(as.shiny.appobj,list)
S3method(as.shiny.appobj,shiny.appobj)
S3method(as.tags,shiny.appobj)
S3method(as.tags,shiny.render.function)
S3method(format,reactiveExpr)
S3method(format,reactiveVal)
S3method(names,reactivevalues)
S3method(print,key_missing)
S3method(print,reactive)
S3method(print,shiny.appobj)
S3method(str,reactivevalues)
@@ -38,6 +41,7 @@ export(actionButton)
export(actionLink)
export(addResourcePath)
export(animationOptions)
export(appendTab)
export(as.shiny.appobj)
export(basicPage)
export(bookmarkButton)
@@ -56,12 +60,15 @@ export(code)
export(column)
export(conditionStackTrace)
export(conditionalPanel)
export(createRenderFunction)
export(createWebDependency)
export(dataTableOutput)
export(dateInput)
export(dateRangeInput)
export(dblclickOpts)
export(debounce)
export(dialogViewer)
export(diskCache)
export(div)
export(downloadButton)
export(downloadHandler)
@@ -69,6 +76,7 @@ export(downloadLink)
export(em)
export(enableBookmarking)
export(eventReactive)
export(exportTestValues)
export(exprToFunction)
export(extractStackTrace)
export(fileInput)
@@ -82,9 +90,13 @@ export(flowLayout)
export(fluidPage)
export(fluidRow)
export(formatStackTrace)
export(freezeReactiveVal)
export(freezeReactiveValue)
export(getCurrentOutputInfo)
export(getDefaultReactiveDomain)
export(getQueryString)
export(getShinyOption)
export(getUrlHash)
export(h1)
export(h2)
export(h3)
@@ -93,6 +105,7 @@ export(h5)
export(h6)
export(headerPanel)
export(helpText)
export(hideTab)
export(hoverOpts)
export(hr)
export(htmlOutput)
@@ -107,15 +120,19 @@ export(includeMarkdown)
export(includeScript)
export(includeText)
export(inputPanel)
export(insertTab)
export(insertUI)
export(installExprFunction)
export(invalidateLater)
export(is.key_missing)
export(is.reactive)
export(is.reactivevalues)
export(is.shiny.appobj)
export(is.singleton)
export(isRunning)
export(isTruthy)
export(isolate)
export(key_missing)
export(knit_print.html)
export(knit_print.reactive)
export(knit_print.shiny.appobj)
@@ -126,6 +143,7 @@ export(mainPanel)
export(makeReactiveBinding)
export(markRenderFunction)
export(maskReactiveContext)
export(memoryCache)
export(modalButton)
export(modalDialog)
export(navbarMenu)
@@ -145,6 +163,7 @@ export(onReactiveDomainEnded)
export(onRestore)
export(onRestored)
export(onSessionEnded)
export(onStop)
export(outputOptions)
export(p)
export(pageWithSidebar)
@@ -154,6 +173,7 @@ export(passwordInput)
export(plotOutput)
export(plotPNG)
export(pre)
export(prependTab)
export(printError)
export(printStackTrace)
export(radioButtons)
@@ -166,13 +186,16 @@ export(reactiveTable)
export(reactiveText)
export(reactiveTimer)
export(reactiveUI)
export(reactiveVal)
export(reactiveValues)
export(reactiveValuesToList)
export(registerInputHandler)
export(removeInputHandler)
export(removeModal)
export(removeNotification)
export(removeTab)
export(removeUI)
export(renderCachedPlot)
export(renderDataTable)
export(renderImage)
export(renderPlot)
@@ -195,6 +218,7 @@ export(selectizeInput)
export(serverInfo)
export(setBookmarkExclude)
export(setProgress)
export(setSerializer)
export(shinyApp)
export(shinyAppDir)
export(shinyAppFile)
@@ -205,10 +229,15 @@ export(showBookmarkUrlModal)
export(showModal)
export(showNotification)
export(showReactLog)
export(showTab)
export(sidebarLayout)
export(sidebarPanel)
export(singleton)
export(sizeGrowthRatio)
export(sliderInput)
export(snapshotExclude)
export(snapshotPreprocessInput)
export(snapshotPreprocessOutput)
export(span)
export(splitLayout)
export(stopApp)
@@ -228,6 +257,7 @@ export(tags)
export(textAreaInput)
export(textInput)
export(textOutput)
export(throttle)
export(titlePanel)
export(uiOutput)
export(updateActionButton)
@@ -246,9 +276,13 @@ export(updateSliderInput)
export(updateTabsetPanel)
export(updateTextAreaInput)
export(updateTextInput)
export(updateVarSelectInput)
export(updateVarSelectizeInput)
export(urlModal)
export(validate)
export(validateCssUnit)
export(varSelectInput)
export(varSelectizeInput)
export(verbatimTextOutput)
export(verticalLayout)
export(wellPanel)
@@ -264,3 +298,5 @@ import(httpuv)
import(methods)
import(mime)
import(xtable)
importFrom(grDevices,dev.cur)
importFrom(grDevices,dev.set)

474
NEWS.md
View File

@@ -1,5 +1,441 @@
shiny 1.2.0
===========
This release features plot caching, an important new tool for improving performance and scalability. Using `renderCachedPlot` in place of `renderPlot` can greatly improve responsiveness for apps that show the same plot many times (for example, a dashboard or report where all users view the same data). Shiny gives you a fair amount of control in where the cache is stored and how cached plots are invalidated, so be sure to read [this article](http://shiny.rstudio.com/articles/plot-caching.html) to get the most out of this feature.
## Full changelog
### New features
* Added `renderCachedPlot()`, which stores plots in a cache so that they can be served up almost instantly. ([#1997](https://github.com/rstudio/shiny/pull/1997))
### Minor new features and improvements
* Upgrade FontAwesome from 4.7.0 to 5.3.1 and made `icon` tags browsable [#2186](https://github.com/rstudio/shiny/issues/2186).
* Address [#174](https://github.com/rstudio/shiny/issues/174): Added `datesdisabled` and `daysofweekdisabled` as new parameters to `dateInput()`. This resolves [#174](https://github.com/rstudio/shiny/issues/174) and exposes the underlying arguments of [Bootstrap Datepicker](http://bootstrap-datepicker.readthedocs.io/en/latest/options.html#datesdisabled). `datesdisabled` expects a character vector with values in `yyyy/mm/dd` format and `daysofweekdisabled` expects an integer vector with day interger ids (Sunday=0, Saturday=6). The default value for both is `NULL`, which leaves all days selectable. Thanks, @nathancday! ([#2147](https://github.com/rstudio/shiny/pull/2147))
* Support for selecting variables of a data frame with the output values to be used within tidy evaluation. Added functions: `varSelectInput`, `varSelectizeInput`, `updateVarSelectInput`, `updateVarSelectizeInput`. ([#2091](https://github.com/rstudio/shiny/pull/2091))
* Addressed [#2042](https://github.com/rstudio/shiny/issues/2042): dates outside of `min`/`max` date range are now a lighter shade of grey to highlight the allowed range. ([#2087](https://github.com/rstudio/shiny/pull/2087))
* Added support for plot interaction when the plot is scaled. ([#2125](https://github.com/rstudio/shiny/pull/2125))
* Fixed [#1933](https://github.com/rstudio/shiny/issues/1933): extended server-side selectize to lists and optgroups. ([#2102](https://github.com/rstudio/shiny/pull/2102))
* Added namespace support when freezing reactiveValue keys. [#2080](https://github.com/rstudio/shiny/pull/2080)
* Upgrade selectize.js from 0.12.1 to 0.12.4 [#2028](https://github.com/rstudio/shiny/issues/2028)
* Addressed [#2079](https://github.com/rstudio/shiny/issues/2079): Added `coords_img`, `coords_css`, and `img_css_ratio` fields containing x and y location information for plot brush, hover, and click events. [#2183](https://github.com/rstudio/shiny/pull/2183)
### Bug fixes
* Fixed [#2033](https://github.com/rstudio/shiny/issues/2033): RStudio Viewer window not closed on `shiny::stopApp()`. Thanks, @vnijs! [#2047](https://github.com/rstudio/shiny/pull/2047)
* Fixed [#1935](https://github.com/rstudio/shiny/issues/1935): correctly returns plot coordinates when using outer margins. ([#2108](https://github.com/rstudio/shiny/pull/2108))
* Resolved [#2019](https://github.com/rstudio/shiny/issues/2019): `updateSliderInput` now changes the slider formatting if the input type changes. ([#2099](https://github.com/rstudio/shiny/pull/2099))
* Fixed [#2138](https://github.com/rstudio/shiny/issues/2138): Inputs that are part of a `renderUI` were no longer restoring correctly from bookmarked state. [#2139](https://github.com/rstudio/shiny/pull/2139)
* Fixed [#2093](https://github.com/rstudio/shiny/issues/2093): Make sure bookmark scope directory does not exist before trying to create it. [#2168](https://github.com/rstudio/shiny/pull/2168)
* Fixed [#2177](https://github.com/rstudio/shiny/issues/2177): The session name is now being recorded when exiting a context. Multiple sessions can now view their respective reactlogs. [#2180](https://github.com/rstudio/shiny/pull/2180)
* Fixed [#2162](https://github.com/rstudio/shiny/issues/2162): `selectInput` was sending spurious duplicate values to the server when using backspace. Thanks, @sada1993! [#2187](https://github.com/rstudio/shiny/pull/2187)
* Fixed [#2142](https://github.com/rstudio/shiny/issues/2142): Dropping files on `fileInput`s stopped working on recent releases of Firefox. Thanks @dmenne for reporting! [#2203](https://github.com/rstudio/shiny/pull/2203)
* Fixed [#2204](https://github.com/rstudio/shiny/issues/2204): `updateDateInput` could set the wrong date on days where DST begins. [#2211](https://github.com/rstudio/shiny/pull/2211)
### Documentation Updates
* Addressed [#1864](https://github.com/rstudio/shiny/issues/1864) by changing `optgroup` documentation to use `list` instead of `c`. ([#2084](https://github.com/rstudio/shiny/pull/2084))
shiny 1.1.0
===========
This is a significant release for Shiny, with a major new feature that was nearly a year in the making: support for asynchronous operations! Until now, R's single-threaded nature meant that performing long-running calculations or tasks from Shiny would bring your app to a halt for other users of that process. This release of Shiny deeply integrates the [promises](https://rstudio.github.io/promises/) package to allow you to execute some tasks asynchronously, including as part of reactive expressions and outputs. See the [promises](https://rstudio.github.io/promises/) documentation to learn more.
## Full changelog
### Breaking changes
* `extractStackTrace` and `formatStackTrace` are deprecated and will be removed in a future version of Shiny. As far as we can tell, nobody has been using these functions, and a refactor has made them vestigial; if you need this functionality, please file an issue.
### New features
* Support for asynchronous operations! Built-in render functions that expected a certain kind of object to be yielded from their `expr`, now generally can handle a promise for that kind of object. Reactive expressions and observers are now promise-aware as well. ([#1932](https://github.com/rstudio/shiny/pull/1932))
* Introduced two changes to the (undocumented but widely used) JavaScript function `Shiny.onInputChange(name, value)`. First, we changed the function name to `Shiny.setInputValue` (but don't worry--the old function name will continue to work). Second, until now, all calls to `Shiny.onInputChange(inputId, value)` have been "deduplicated"; that is, anytime an input is set to the same value it already has, the set is ignored. With Shiny v1.1, you can now add an options object as the third parameter: `Shiny.setInputValue("name", value, {priority: "event"})`. When the priority option is set to `"event"`, Shiny will always send the value and trigger reactivity, whether it is a duplicate or not. This closes [#928](https://github.com/rstudio/shiny/issues/928), which was the most upvoted open issue by far! Thanks, @daattali. ([#2018](https://github.com/rstudio/shiny/pull/2018))
### Minor new features and improvements
* Addressed [#1978](https://github.com/rstudio/shiny/issues/1978): `shiny:value` is now triggered when duplicate output data is received from the server. (Thanks, @andrewsali! [#1999](https://github.com/rstudio/shiny/pull/1999))
* If a shiny output contains a css class of `shiny-report-size`, its container height and width are now reported in `session$clientData`. So, for an output with an id with `"myID"`, the height/width can be accessed via `session$clientData[['output_myID_height']]`/`session$clientData[['output_myID_width']]`. Addresses [#1980](https://github.com/rstudio/shiny/issues/1980). (Thanks, @cpsievert! [#1981](https://github.com/rstudio/shiny/pull/1981))
* Added a new `autoclose = TRUE` parameter to `dateInput()` and `dateRangeInput()`. This closed [#1969](https://github.com/rstudio/shiny/issues/1969) which was a duplicate of much older issue, [#173](https://github.com/rstudio/shiny/issues/173). The default value is `TRUE` since that seems to be the common use case. However, this will cause existing apps with date inputs (that update to this version of Shiny) to have the datepicker be immediately closed once a date is selected. For most apps, this is actually desired behavior; if you wish to keep the datepicker open until the user clicks out of it use `autoclose = FALSE`. ([#1987](https://github.com/rstudio/shiny/pull/1987))
* The version of Shiny is now accessible from Javascript, with `Shiny.version`. There is also a new function for comparing version strings, `Shiny.compareVersion()`. ([#1826](https://github.com/rstudio/shiny/pull/1826), [#1830](https://github.com/rstudio/shiny/pull/1830))
* Addressed [#1851](https://github.com/rstudio/shiny/issues/1851): Stack traces are now smaller in some places `do.call()` is used. ([#1856](https://github.com/rstudio/shiny/pull/1856))
* Stack traces have been improved, with more aggressive de-noising and support for deep stack traces (stitching together multiple stack traces that are conceptually part of the same async operation).
* Addressed [#1859](https://github.com/rstudio/shiny/issues/1859): Server-side selectize is now significantly faster. (Thanks to @dselivanov [#1861](https://github.com/rstudio/shiny/pull/1861))
* [#1989](https://github.com/rstudio/shiny/issues/1989): The server side of outputs can now be removed (e.g. `output$plot <- NULL`). This is not usually necessary but it does allow some objects to be garbage collected, which might matter if you are dynamically creating and destroying many outputs. (Thanks, @mmuurr! [#2011](https://github.com/rstudio/shiny/pull/2011))
* Removed the (ridiculously outdated) "experimental feature" tag from the reference documentation for `renderUI`. ([#2036](https://github.com/rstudio/shiny/pull/2036))
* Addressed [#1907](https://github.com/rstudio/shiny/issues/1907): the `ignoreInit` argument was first added only to `observeEvent`. Later, we also added it to `eventReactive`, but forgot to update the documentation. Now done, thanks [@flo12392](https://github.com/flo12392)! ([#2036](https://github.com/rstudio/shiny/pull/2036))
### Bug fixes
* Fixed [#1006](https://github.com/rstudio/shiny/issues/1006): Slider inputs sometimes showed too many digits. ([#1956](https://github.com/rstudio/shiny/pull/1956))
* Fixed [#1958](https://github.com/rstudio/shiny/issues/1958): Slider inputs previously displayed commas after a decimal point. ([#1960](https://github.com/rstudio/shiny/pull/1960))
* The internal `URLdecode()` function previously was a copy of `httpuv::decodeURIComponent()`, assigned at build time; now it invokes the httpuv function at run time.
* Fixed [#1840](https://github.com/rstudio/shiny/issues/1840): with the release of Shiny 1.0.5, we accidently changed the relative positioning of the icon and the title text in `navbarMenu`s and `tabPanel`s. This fix reverts this behavior back (i.e. the icon should be to the left of the text and/or the downward arrow in case of `navbarMenu`s). ([#1848](https://github.com/rstudio/shiny/pull/1848))
* Fixed [#1600](https://github.com/rstudio/shiny/issues/1600): URL-encoded bookmarking did not work with sliders that had dates or date-times. ([#1961](https://github.com/rstudio/shiny/pull/1961))
* Fixed [#1962](https://github.com/rstudio/shiny/issues/1962): [File dragging and dropping](https://blog.rstudio.com/2017/08/15/shiny-1-0-4/) broke in the presence of jQuery version 3.0 as introduced by the [rhandsontable](https://jrowen.github.io/rhandsontable/) [htmlwidget](https://www.htmlwidgets.org/). ([#2005](https://github.com/rstudio/shiny/pull/2005))
* Improved the error handling inside the `addResourcePath()` function, to give end users more informative error messages when the `directoryPath` argument cannot be normalized. This is especially useful for `runtime: shiny_prerendered` Rmd documents, like `learnr` tutorials. ([#1968](https://github.com/rstudio/shiny/pull/1968))
* Changed script tags in reactlog ([inst/www/reactive-graph.html](https://github.com/rstudio/shiny/blob/master/inst/www/reactive-graph.html)) from HTTP to HTTPS in order to avoid mixed content blocking by most browsers. (Thanks, @jekriske-lilly! [#1844](https://github.com/rstudio/shiny/pull/1844))
* Addressed [#1784](https://github.com/rstudio/shiny/issues/1784): `runApp()` will avoid port 6697, which is considered unsafe by Chrome.
* Fixed [#2000](https://github.com/rstudio/shiny/issues/2000): Implicit calls to `xxxOutput` not working inside modules. (Thanks, @GregorDeCillia! [#2010](https://github.com/rstudio/shiny/pull/2010))
* Fixed [#2021](https://github.com/rstudio/shiny/issues/2021): Memory leak with `reactiveTimer` and `invalidateLater`. ([#2022](https://github.com/rstudio/shiny/pull/2022))
### Library updates
* Updated to ion.rangeSlider 2.2.0. ([#1955](https://github.com/rstudio/shiny/pull/1955))
## Known issues
In some rare cases, interrupting an application (by pressing Ctrl-C or Esc) may result in the message `Error in execCallbacks(timeoutSecs) : c++ exception (unknown reason)`. Although this message sounds alarming, it is harmless, and will go away in a future version of the later package (more information [here](https://github.com/r-lib/later/issues/55)).
shiny 1.0.5
===========
## Full changelog
### Bug fixes
* Fixed [#1818](https://github.com/rstudio/shiny/issues/1818): `conditionalPanel()` expressions that have a newline character in them caused the application to not work. ([#1820](https://github.com/rstudio/shiny/pull/1820))
* Added a safe wrapper function for internal calls to `jsonlite::fromJSON()`. ([#1822](https://github.com/rstudio/shiny/pull/1822))
* Fixed [#1824](https://github.com/rstudio/shiny/issues/1824): HTTP HEAD requests on static files caused the application to stop. ([#1825](https://github.com/rstudio/shiny/pull/1825))
shiny 1.0.4
===========
There are three headlining features in this release of Shiny. It is now possible to add and remove tabs from a `tabPanel`; there is a new function, `onStop()`, which registers callbacks that execute when an application exits; and `fileInput`s now can have files dragged and dropped on them. In addition to these features, this release has a number of minor features and bug fixes. See the full changelog below for more details.
## Full changelog
### New features
* Implemented [#1668](https://github.com/rstudio/shiny/issues/1668): dynamic tabs: added functions (`insertTab`, `appendTab`, `prependTab`, `removeTab`, `showTab` and `hideTab`) that allow you to do those actions for an existing `tabsetPanel`. ([#1794](https://github.com/rstudio/shiny/pull/1794))
* Implemented [#1213](https://github.com/rstudio/shiny/issues/1213): Added a new function, `onStop()`, which can be used to register callback functions that are invoked when an application exits, or when a user session ends. (Multiple sessions can be connected to a single running Shiny application.) This is useful if you have finalization/clean-up code that should be run after the application exits. ([#1770](https://github.com/rstudio/shiny/pull/1770)
* Implemented [#1155](https://github.com/rstudio/shiny/issues/1155): Files can now be drag-and-dropped on `fileInput` controls. The appearance of `fileInput` controls while files are being dragged can be modified by overriding the `shiny-file-input-active` and `shiny-file-input-over` classes. ([#1782](https://github.com/rstudio/shiny/pull/1782))
### Minor new features and improvements
* Addressed [#1688](https://github.com/rstudio/shiny/issues/1688): trigger a new `shiny:outputinvalidated` event when an output gets invalidated, at the same time that the `recalculating` CSS class is added. ([#1758](https://github.com/rstudio/shiny/pull/1758), thanks [@andrewsali](https://github.com/andrewsali)!)
* Addressed [#1508](https://github.com/rstudio/shiny/issues/1508): `fileInput` now permits the same file to be uploaded multiple times. ([#1719](https://github.com/rstudio/shiny/pull/1719))
* Addressed [#1501](https://github.com/rstudio/shiny/issues/1501): The `fileInput` control now retains uploaded file extensions on the server. This fixes [readxl](https://github.com/tidyverse/readxl)'s `readxl::read_excel` and other functions that must recognize a file's extension in order to work. ([#1706](https://github.com/rstudio/shiny/pull/1706))
* For `conditionalPanel`s, Shiny now gives more informative messages if there are errors evaluating or parsing the JavaScript conditional expression. ([#1727](https://github.com/rstudio/shiny/pull/1727))
* Addressed [#1586](https://github.com/rstudio/shiny/issues/1586): The `conditionalPanel` function now accepts an `ns` argument. The `ns` argument can be used in a [module](https://shiny.rstudio.com/articles/modules.html) UI function to scope the `condition` expression to the module's own input and output IDs. ([#1735](https://github.com/rstudio/shiny/pull/1735))
* With `options(shiny.testmode=TRUE)`, the Shiny process will send a message to the client in response to a changed input, even if no outputs have changed. This helps to streamline testing using the shinytest package. ([#1747](https://github.com/rstudio/shiny/pull/1747))
* Addressed [#1738](https://github.com/rstudio/shiny/issues/1738): The `updateTextInput` and `updateTextAreaInput` functions can now update the placeholder. ([#1742](https://github.com/rstudio/shiny/pull/1742))
* Converted examples to single file apps, and made updates and enhancements to comments in the example app scripts. ([#1685](https://github.com/rstudio/shiny/pull/1685))
* Added new `snapshotPreprocessInput()` and `snapshotPreprocessOutput()` functions, which is used for preprocessing and input and output values before taking a test snapshot. ([#1760](https://github.com/rstudio/shiny/pull/1760), [#1789](https://github.com/rstudio/shiny/pull/1789))
* The HTML generated by `renderTable()` no longer includes comments with the R version, xtable version, and timestamp. ([#1771](https://github.com/rstudio/shiny/pull/1771))
* Added a function `isRunning` to test whether a Shiny app is currently running. ([#1785](https://github.com/rstudio/shiny/pull/1785))
* Added a function `setSerializer`, which allows authors to specify a function for serializing the value of a custom input. ([#1791](https://github.com/rstudio/shiny/pull/1791))
### Bug fixes
* Fixed [#1546](https://github.com/rstudio/shiny/issues/1546): make it possible (without any hacks) to write arbitrary data into a module's `session$userData` (which is exactly the same environment as the parent's `session$userData`). To be clear, it allows something like `session$userData$x <- TRUE`, but not something like `session$userData <- TRUE` (that is not allowed in any context, whether you're in the main app, or in a module) ([#1732](https://github.com/rstudio/shiny/pull/1732)).
* Fixed [#1701](https://github.com/rstudio/shiny/issues/1701): There was a partial argument match in the `generateOptions` function. ([#1702](https://github.com/rstudio/shiny/pull/1702))
* Fixed [#1710](https://github.com/rstudio/shiny/issues/1710): `ReactiveVal` objects did not have separate dependents. ([#1712](https://github.com/rstudio/shiny/pull/1712))
* Fixed [#1438](https://github.com/rstudio/shiny/issues/1438): `unbindAll()` should not be called when inserting content with `insertUI()`. A previous fix ([#1449](https://github.com/rstudio/shiny/pull/1449)) did not work correctly. ([#1736](https://github.com/rstudio/shiny/pull/1736))
* Fixed [#1755](https://github.com/rstudio/shiny/issues/1755): dynamic htmlwidgets sent the path of the package on the server to the client. ([#1756](https://github.com/rstudio/shiny/pull/1756))
* Fixed [#1763](https://github.com/rstudio/shiny/issues/1763): Shiny's private random stream leaked out into the main random stream. ([#1768](https://github.com/rstudio/shiny/pull/1768))
* Fixed [#1680](https://github.com/rstudio/shiny/issues/1680): `options(warn=2)` was not respected when running an app. ([#1790](https://github.com/rstudio/shiny/pull/1790))
* Fixed [#1772](https://github.com/rstudio/shiny/issues/1772): ensure that `runApp()` respects the `shinyApp(onStart = function())` argument. ([#1770](https://github.com/rstudio/shiny/pull/1770))
* Fixed [#1474](https://github.com/rstudio/shiny/issues/1474): A `browser()` call in an observer could cause an error in the RStudio IDE on Windows. ([#1802](https://github.com/rstudio/shiny/pull/1802))
shiny 1.0.3
================
This is a hotfix release of Shiny. With previous versions of Shiny, when running an application on the newly-released version of R, 3.4.0, it would print a message: `Warning in body(fun) : argument is not a function`. This has no effect on the application, but because the message could be alarming to users, we are releasing a new version of Shiny that fixes this issue.
## Full changelog
### Bug fixes
* Fixed [#1672](https://github.com/rstudio/shiny/issues/1672): When an error occurred while uploading a file, the progress bar did not change colors. ([#1673](https://github.com/rstudio/shiny/pull/1673))
* Fixed [#1676](https://github.com/rstudio/shiny/issues/1676): On R 3.4.0, running a Shiny application gave a warning: `Warning in body(fun) : argument is not a function`. ([#1677](https://github.com/rstudio/shiny/pull/1677))
shiny 1.0.2
================
This is a hotfix release of Shiny. The primary reason for this release is because the web host for MathJax JavaScript library is scheduled to be shut down in the next few weeks. After it is shut down, Shiny applications that use MathJax will no longer be able to load the MathJax library if they are run with Shiny 1.0.1 and below. (If you don't know whether your application uses MathJax, it probably does not.) For more information about why the MathJax CDN is shutting down, see https://www.mathjax.org/cdn-shutting-down/.
## Full changelog
### Minor new features and improvements
* Added a `shiny:sessioninitialized` Javascript event, which is fired at the end of the initialize method of the Session object. This allows us to listen for this event when we want to get the value of things like `Shiny.user`. ([#1568](https://github.com/rstudio/shiny/pull/1568))
* Fixed [#1649](https://github.com/rstudio/shiny/issues/1649): allow the `choices` argument in `checkboxGroupInput()` to be `NULL` (or `c()`) to keep backward compatibility with Shiny < 1.0.1. This will result in the same thing as providing `choices = character(0)`. ([#1652](https://github.com/rstudio/shiny/pull/1652))
* The official URL for accessing MathJax libraries over CDN has been deprecated and will be removed soon. We have switched to a new rstudio.com URL that we will support going forward. ([#1664](https://github.com/rstudio/shiny/pull/1664))
### Bug fixes
* Fixed [#1653](https://github.com/rstudio/shiny/issues/1653): wrong code example in documentation. ([#1658](https://github.com/rstudio/shiny/pull/1658))
shiny 1.0.1
================
This is a maintenance release of Shiny, mostly aimed at fixing bugs and introducing minor features. The most notable additions in this version of Shiny are the introduction of the `reactiveVal()` function (it's like `reactiveValues()`, but it only stores a single value), and that the choices of `radioButtons()` and `checkboxGroupInput()` can now contain HTML content instead of just plain text.
## Full changelog
### Breaking changes
* The functions `radioButtons()`, `checkboxGroupInput()` and `selectInput()` (and the corresponding `updateXXX()` functions) no longer accept a `selected` argument whose value is the name of a choice, instead of the value of the choice. This feature had been deprecated since Shiny 0.10 (it printed a warning message, but still tried to match the name to the right choice) and it's now completely unsupported.
### New features
* Added `reactiveVal` function, for storing a single value which can be (reactively) read and written. Similar to `reactiveValues`, except that `reactiveVal` just lets you store a single value instead of storing multiple values by name. ([#1614](https://github.com/rstudio/shiny/pull/1614))
### Minor new features and improvements
* Fixed [#1637](https://github.com/rstudio/shiny/issues/1637): Outputs stay faded on MS Edge. ([#1640](https://github.com/rstudio/shiny/pull/1640))
* Addressed [#1348](https://github.com/rstudio/shiny/issues/1348) and [#1437](https://github.com/rstudio/shiny/issues/1437) by adding two new arguments to `radioButtons()` and `checkboxGroupInput()`: `choiceNames` (list or vector) and `choiceValues` (list or vector). These can be passed in as an alternative to `choices`, with the added benefit that the elements in `choiceNames` can be arbitrary UI (i.e. anything created by `HTML()` and the `tags()` functions, like icons and images). While the underlying values for each choice (passed in through `choiceValues`) must still be simple text, their visual representation on the app (what the user actually clicks to select a different option) can be any valid HTML element. See `?radioButtons` for a small example. ([#1521](https://github.com/rstudio/shiny/pull/1521))
* Updated `tools/README.md` with more detailed instructions. ([#1616](https://github.com/rstudio/shiny/pull/1616))
* Fixed [#1565](https://github.com/rstudio/shiny/issues/1565), which meant that resources with spaces in their names return HTTP 404. ([#1566](https://github.com/rstudio/shiny/pull/1566))
* Exported `session$user` (if it exists) to the client-side; it's accessible in the Shiny object: `Shiny.user`. ([#1563](https://github.com/rstudio/shiny/pull/1563))
* Added support for HTML5's `pushState` which allows for pseudo-navigation
in shiny apps. For more info, see the documentation (`?updateQueryString` and `?getQueryString`). ([#1447](https://github.com/rstudio/shiny/pull/1447))
* Fixed [#1121](https://github.com/rstudio/shiny/issues/1121): plot interactions with ggplot2 now support `coord_fixed()`. ([#1525](https://github.com/rstudio/shiny/pull/1525))
* Added `snapshotExclude` function, which marks an output so that it is not recorded in a test snapshot. ([#1559](https://github.com/rstudio/shiny/pull/1559))
* Added `shiny:filedownload` JavaScript event, which is triggered when a `downloadButton` or `downloadLink` is clicked. Also, the values of `downloadHandler`s are not recorded in test snapshots, because the values change every time the application is run. ([#1559](https://github.com/rstudio/shiny/pull/1559))
* Added support for plot interactions with ggplot2 > 2.2.1. ([#1578](https://github.com/rstudio/shiny/pull/1578))
* Fixed [#1577](https://github.com/rstudio/shiny/issues/1577): Improved `escapeHTML` (util.js) in terms of the order dependency of replacing, XSS risk attack and performance. ([#1579](https://github.com/rstudio/shiny/pull/1579))
* The `shiny:inputchanged` JavaScript event now includes two new fields, `binding` and `el`, which contain the input binding and DOM element, respectively. Additionally, `Shiny.onInputChange()` now accepts an optional argument, `opts`, which can contain the same fields. ([#1596](https://github.com/rstudio/shiny/pull/1596))
* The `NS()` function now returns a vectorized function. ([#1613](https://github.com/rstudio/shiny/pull/1613))
* Fixed [#1617](https://github.com/rstudio/shiny/issues/1617): `fileInput` can have customized text for the button and the placeholder. ([#1619](https://github.com/rstudio/shiny/pull/1619))
### Bug fixes
* Fixed [#1511](https://github.com/rstudio/shiny/issues/1511): `fileInput`s did not trigger the `shiny:inputchanged` event on the client. Also removed `shiny:fileuploaded` JavaScript event, because it is no longer needed after this fix. ([#1541](https://github.com/rstudio/shiny/pull/1541), [#1570](https://github.com/rstudio/shiny/pull/1570))
* Fixed [#1472](https://github.com/rstudio/shiny/issues/1472): With a Progress object, calling `set(value=NULL)` made the progress bar go to 100%. Now it does not change the value of the progress bar. The documentation also incorrectly said that setting the `value` to `NULL` would hide the progress bar. ([#1547](https://github.com/rstudio/shiny/pull/1547))
* Fixed [#162](https://github.com/rstudio/shiny/issues/162): When a dynamically-generated input changed to a different `inputType`, it might be incorrectly deduplicated. ([#1594](https://github.com/rstudio/shiny/pull/1594))
* Removed redundant call to `inputs.setInput`. ([#1595](https://github.com/rstudio/shiny/pull/1595))
* Fixed bug where `dateRangeInput` did not respect `weekstart` argument. ([#1592](https://github.com/rstudio/shiny/pull/1592))
* Fixed [#1598](https://github.com/rstudio/shiny/issues/1598): `setBookmarkExclude()` did not work properly inside of modules. ([#1599](https://github.com/rstudio/shiny/pull/1599))
* Fixed [#1605](https://github.com/rstudio/shiny/issues/1605): sliders did not move when clicked on the bar area. ([#1610](https://github.com/rstudio/shiny/pull/1610))
* Fixed [#1621](https://github.com/rstudio/shiny/issues/1621): if a `reactiveTimer`'s session was closed before the first time that the `reactiveTimer` fired, then the `reactiveTimer` would not get cleared and would keep firing indefinitely. ([#1623](https://github.com/rstudio/shiny/pull/1623))
* Fixed [#1634](https://github.com/rstudio/shiny/issues/1634): If brushing on a plot causes the plot to redraw, then the redraw could in turn trigger the plot to redraw again and again. This was due to spurious changes in values of floating point numbers. ([#1641](https://github.com/rstudio/shiny/pull/1641))
### Library updates
* Closed [#1500](https://github.com/rstudio/shiny/issues/1500): Updated ion.rangeSlider to 2.1.6. ([#1540](https://github.com/rstudio/shiny/pull/1540))
shiny 1.0.0
===========
Shiny has reached a milestone: version 1.0.0! In the last year, we've added two major features that we considered essential for a 1.0.0 release: bookmarking, and support for testing Shiny applications. As usual, this version of Shiny also includes many minor features and bug fixes.
Here are some highlights from this release. For more details, see the full changelog below.
## Support for testing Shiny applications
Shiny now supports automated testing of applications, with the [shinytest](https://github.com/rstudio/shinytest) package. Shinytest has not yet been released on CRAN, but will be soon. ([#18](https://github.com/rstudio/shiny/issues/18), [#1464](https://github.com/rstudio/shiny/pull/1464))
## Debounce/throttle reactives
Now there's an official way to slow down reactive values and expressions that invalidate too quickly. Pass a reactive expression to the new `debounce` or `throttle` function, and get back a modified reactive expression that doesn't invalidate as often. ([#1510](https://github.com/rstudio/shiny/pull/1510))
## Full changelog
### Breaking changes
* Added a new `placeholder` argument to `verbatimTextOutput()`. The default is `FALSE`, which means that, if there is no content for this output, no representation of this slot will be made in the UI. Previsouly, even if there was no content, you'd see an empty rectangle in the UI that served as a placeholder. You can set `placeholder = TRUE` to revert back to that look. ([#1480](https://github.com/rstudio/shiny/pull/1480))
### New features
* Added support for testing Shiny applications with the shinytest package. ([#18](https://github.com/rstudio/shiny/issues/18), [#1464](https://github.com/rstudio/shiny/pull/1464))
* Added `debounce` and `throttle` functions, to control the rate at which reactive values and expressions invalidate. ([#1510](https://github.com/rstudio/shiny/pull/1510))
### Minor new features and improvements
* Addressed [#1486](https://github.com/rstudio/shiny/issues/1486) by adding a new argument to `observeEvent` and `eventReactive`, called `ignoreInit` (defaults to `FALSE` for backwards compatibility). When set to `TRUE`, the action (i.e. the second argument: `handlerExpr` and `valueExpr`, respectively) will not be triggered when the observer/reactive is first created/initialized. In other words, `ignoreInit = TRUE` ensures that the `observeEvent` (or `eventReactive`) is *never* run right away. For more info, see the documentation (`?observeEvent`). ([#1494](https://github.com/rstudio/shiny/pull/1494))
* Added a new argument to `observeEvent` called `once`. When set to `TRUE`, it results in the observer being destroyed (stop observing) after the first time that `handlerExpr` is run (i.e. `once = TRUE` guarantees that the observer only runs, at most, once). For more info, see the documentation (`?observeEvent`). ([#1494](https://github.com/rstudio/shiny/pull/1494))
* Addressed [#1358](https://github.com/rstudio/shiny/issues/1358): more informative error message when calling `runApp()` inside of an app's app.R (or inside ui.R or server.R). ([#1482](https://github.com/rstudio/shiny/pull/1482))
* Added a more descriptive JS warning for `insertUI()` when the selector argument does not match anything in DOM. ([#1488](https://github.com/rstudio/shiny/pull/1488))
* Added support for injecting JavaScript code when the `shiny.testmode` option is set to `TRUE`. This makes it possible to record test events interactively. ([#1464](https://github.com/rstudio/shiny/pull/1464))
* Added ability through arguments to the `a` tag function called inside `downloadButton()` and `downloadLink()`. Closes [#986](https://github.com/rstudio/shiny/issues/986). ([#1492](https://github.com/rstudio/shiny/pulls/1492))
* Implemented [#1512](https://github.com/rstudio/shiny/issues/1512): added a `userData` environment to `session`, for storing arbitrary session-related variables. Generally, session-scoped variables are created just by declaring normal variables that are local to the Shiny server function, but `session$userData` may be more convenient for some advanced scenarios. ([#1513](https://github.com/rstudio/shiny/pull/1513))
* Relaxed naming requirements for `addResourcePath()` (the first character no longer needs to be a letter). ([#1529](https://github.com/rstudio/shiny/pull/1529))
### Bug fixes
* Fixed [#969](https://github.com/rstudio/shiny/issues/969): allow navbarPage's `fluid` param to control both containers. ([#1481](https://github.com/rstudio/shiny/pull/1481))
* Fixed [#1438](https://github.com/rstudio/shiny/issues/1438): `unbindAll()` should not be called when inserting content with `insertUI()` ([#1449](https://github.com/rstudio/shiny/pull/1449))
* Fixed bug causing `<meta>` tags associated with HTML dependencies of Shiny R Markdown files to be rendered incorrectly. ([#1463](https://github.com/rstudio/shiny/pull/1463))
* Fixed [#1359](https://github.com/rstudio/shiny/issues/1359): `shinyApp()` options argument ignored when passed to `runApp()`. ([#1483](https://github.com/rstudio/shiny/pull/1483))
* Fixed [#117](https://github.com/rstudio/shiny/issues/117): Reactive expressions now release references to cached values as soon as they are invalidated, potentially making those cached values eligible for garbage collection sooner. Previously, this would not occur until the next cached value was calculated and stored. ([#1504](https://github.com/rstudio/shiny/pull/1504/files))
* Fixed [#1013](https://github.com/rstudio/shiny/issues/1013): `flushReact` should be called after app loads. Observers set up outside of server functions were not running until after the first user connects. ([#1503](https://github.com/rstudio/shiny/pull/1503))
* Fixed [#1453](https://github.com/rstudio/shiny/issues/1453): When using a modal dialog with `easyClose=TRUE` in a Shiny gadget, pressing Esc would close both the modal and the gadget. Now pressing Esc only closes the modal. ([#1523](https://github.com/rstudio/shiny/pull/1523))
### Library updates
* Updated to Font Awesome 4.7.0.
shiny 0.14.2
============
This is a maintenance release of Shiny, with some bug fixes and minor new features.
## Full changelog
### Minor new features and improvements
* Added a `fade` argument to `modalDialog()` -- setting it to `FALSE` will remove the usual fade-in animation for that modal window. ([#1414](https://github.com/rstudio/shiny/pull/1414))
* Fixed a "duplicate binding" error that occurred in some edge cases involving `insertUI` and nested `uiOutput`. ([#1402](https://github.com/rstudio/shiny/pull/1402))
* Fixed [#1422](https://github.com/rstudio/shiny/issues/1422): When using the `shiny.trace` option, allow specifying to only log SEND or RECV messages, or both. (PR [#1428](https://github.com/rstudio/shiny/pull/1428))
* Fixed [#1419](https://github.com/rstudio/shiny/issues/1419): Allow overriding a JS custom message handler. (PR [#1445](https://github.com/rstudio/shiny/pull/1445))
* Added `exportTestValues()` function, which allows a test driver to query the session for values internal to an application's server function. This only has an effect if the `shiny.testmode` option is set to `TRUE`. ([#1436](https://github.com/rstudio/shiny/pull/1436))
### Bug fixes
* Fixed [#1427](https://github.com/rstudio/shiny/issues/1427): make sure that modals do not close incorrectly when an element inside them is triggered as hidden. ([#1430](https://github.com/rstudio/shiny/pull/1430))
* Fixed [#1404](https://github.com/rstudio/shiny/issues/1404): stack trace tests were not compatible with the byte-code compiler in R-devel, which now tracks source references.
* `sliderInputBinding.setValue()` now sends a slider's value immediately, instead of waiting for the usual 250ms debounce delay. ([#1429](https://github.com/rstudio/shiny/pull/1429))
* Fixed a bug where, in versions of R before 3.2, Shiny applications could crash due to a bug in R's implementation of `list2env()`. ([#1446](https://github.com/rstudio/shiny/pull/1446))
shiny 0.14.1
============
This is a maintenance release of Shiny, with some bug fixes and minor new features.
## Full changelog
### Minor new features and improvements
* Restored file inputs are now copied on restore, so that the restored application can't modify the bookmarked file. ([#1370](https://github.com/rstudio/shiny/issues/1370))
* Added support for plot interaction in the development version of ggplot2, 2.1.0.9000. Also added support for ggplot2 plots with `coord_flip()` (in the development version of ggplot2). ([hadley/ggplot2#1781](https://github.com/hadley/ggplot2/issues/1781), [#1392](https://github.com/rstudio/shiny/pull/1392))
### Bug fixes
* Fixed [#1093](https://github.com/rstudio/shiny/issues/1093) better: `updateRadioButtons()` and `updateCheckboxGroupInput()` were not working correctly if the choices were given as a numeric vector. This had been solved in [#1291](https://github.com/rstudio/shiny/pull/1291), but that introduced a different bug [#1396](https://github.com/rstudio/shiny/issues/1396) that this better fix avoids. ([#1370](https://github.com/rstudio/shiny/pull/1397))
* Fixed [#1368](https://github.com/rstudio/shiny/issues/1368): If an app with a file input was bookmarked and restored, and then the restored app was bookmarked and restored (without uploading a new file), then it would fail to restore the file the second time. ([#1370](https://github.com/rstudio/shiny/pull/1370))
* Fixed [#1369](https://github.com/rstudio/shiny/issues/1369): `sliderInput()` did not allow showing numbers without a thousands separator.
* Fixed [#1346](https://github.com/rstudio/shiny/issues/1346) and [#1107](https://github.com/rstudio/shiny/issues/1107) : jQuery UI's datepicker conflicted with the bootstrap-datepicker used by Shiny's `dateInput()` and `dateRangeInput()`. ([#1374](https://github.com/rstudio/shiny/pull/1374))
### Library updates
* Updated to bootstrap-datepicker 1.6.4. ([#1218](https://github.com/rstudio/shiny/issues/1218), [#1374](https://github.com/rstudio/shiny/pull/1374))
* Updated to jQuery UI 1.12.1. Previously, Shiny included a build of 1.11.4 which was missing the datepicker component due to a conflict with the bootstrap-datepicker used by Shiny's `dateInput()` and `dateRangeInput()`. ([#1374](https://github.com/rstudio/shiny/pull/1374))
shiny 0.14
=================
==========
A new Shiny release is upon us! There are many new exciting features, bug fixes, and library updates. We'll just highlight the most important changes here, but you can browse through the full changelog below for details. This will likely be the last release before shiny 1.0, so get out your party hats!
@@ -10,17 +446,17 @@ Shiny now supports bookmarkable state: users can save the state of an applicatio
**_Important note_:**
> Saved-to-server bookmarking currently works with Shiny Server Open Source. Support on Shiny Server Pro, RStudio Connect, and shinyapps.io is under development and testing. However, URL-encoded bookmarking works on all hosting platforms.
See [this article](http://shiny.rstudio-staging.com/articles/bookmarking-state.html) to get started with bookmarkable state. There is also an [advanced-level article](http://shiny.rstudio-staging.com/articles/advanced-bookmarking.html) (for apps that have a complex state), and [a modules article](http://shiny.rstudio-staging.com/articles/bookmarking-modules.html) that details how to use bookmarking in conjunction with modules.
See [this article](http://shiny.rstudio.com/articles/bookmarking-state.html) to get started with bookmarkable state. There is also an [advanced-level article](http://shiny.rstudio.com/articles/advanced-bookmarking.html) (for apps that have a complex state), and [a modules article](http://shiny.rstudio.com/articles/bookmarking-modules.html) that details how to use bookmarking in conjunction with modules.
## Notifications
Shiny can now display notifications on the client browser by using the `showNotification()` function. Use [this demo app](https://gallery.shinyapps.io/116-notifications/) to play around with the notification API. Here's a screenshot of a very simple notification (shown when the button is clicked):
<p align="center">
<img src="http://shiny.rstudio-staging.com/images/notification.png" alt="notification" width="50%"/>
<img src="http://shiny.rstudio.com/images/notification.png" alt="notification" width="50%"/>
</p>
[Here](http://shiny.rstudio-staging.com/articles/notifications.html)'s our article about it, and the [reference documentation](http://shiny.rstudio-staging.com/reference/shiny/latest/showNotification.html).
[Here](http://shiny.rstudio.com/articles/notifications.html)'s our article about it, and the [reference documentation](http://shiny.rstudio.com/reference/shiny/latest/showNotification.html).
## Progress indicators
@@ -29,7 +465,7 @@ If your Shiny app contains computations that take a long time to complete, a pro
**_Important note_:**
> If you were already using progress bars and had customized them with your own CSS, you can add the `style = "old"` argument to your `withProgress()` call (or `Progress$new()`). This will result in the same appearance as before. You can also call `shinyOptions(progress.style = "old")` in your app's server function to make all progress indicators use the old styling.
To see new progress bars in action, see [this app](https://gallery.shinyapps.io/085-progress/) in the gallery. You can also learn more about this in [our article](http://shiny.rstudio-staging.com/articles/progress.html) and in the reference documentation (either for the easier [`withProgress` functional API](http://shiny.rstudio-staging.com/reference/shiny/latest/withProgress.html) or the more complicated, but more powerful, [`Progress` object-oriented API](http://shiny.rstudio-staging.com/reference/shiny/latest/Progress.html).
To see new progress bars in action, see [this app](https://gallery.shinyapps.io/085-progress/) in the gallery. You can also learn more about this in [our article](http://shiny.rstudio.com/articles/progress.html) and in the reference documentation (either for the easier [`withProgress` functional API](http://shiny.rstudio.com/reference/shiny/latest/withProgress.html) or the more complicated, but more powerful, [`Progress` object-oriented API](http://shiny.rstudio.com/reference/shiny/latest/Progress.html).
## Reconnection
@@ -40,10 +476,10 @@ Shiny can now automatically reconnect to your Shiny session if you temporarily l
Shiny has now built-in support for displaying modal dialogs like the one below ([live app here](https://gallery.shinyapps.io/114-modal-dialog/)):
<p align="center">
<img src="http://shiny.rstudio-staging.com/images/modal-dialog.png" alt="modal-dialog" width="50%"/>
<img src="http://shiny.rstudio.com/images/modal-dialog.png" alt="modal-dialog" width="50%"/>
</p>
To learn more about this, read [our article](http://shiny.rstudio-staging.com/articles/modal-dialogs.html) and the [reference documentation](http://shiny.rstudio-staging.com/reference/shiny/latest/modalDialog.html).
To learn more about this, read [our article](http://shiny.rstudio.com/articles/modal-dialogs.html) and the [reference documentation](http://shiny.rstudio.com/reference/shiny/latest/modalDialog.html).
## `insertUI` and `removeUI`
@@ -51,13 +487,13 @@ Sometimes in a Shiny app, arbitrary HTML UI may need to be created on-the-fly in
See [this simple demo app](https://gallery.shinyapps.io/111-insert-ui/) of how one could use `insertUI` and `removeUI` to insert and remove text elements using a queue. Also see [this other app](https://gallery.shinyapps.io/insertUI/) that demonstrates how to insert and remove a few common Shiny input objects. Finally, [this app](https://gallery.shinyapps.io/insertUI-modules/) shows how to dynamically insert modules using `insertUI`.
For more, read [our article](http://shiny.rstudio-staging.com/articles/dynamic-ui.html) about dynamic UI generation and the reference documentation about [`insertUI`](http://shiny.rstudio-staging.com/reference/shiny/latest/insertUI.html) and [`removeUI`](http://shiny.rstudio-staging.com/reference/shiny/latest/removeUI.html).
For more, read [our article](http://shiny.rstudio.com/articles/dynamic-ui.html) about dynamic UI generation and the reference documentation about [`insertUI`](http://shiny.rstudio.com/reference/shiny/latest/insertUI.html) and [`removeUI`](http://shiny.rstudio.com/reference/shiny/latest/removeUI.html).
## Documentation for connecting to an external database
Many Shiny users have asked about best practices for accessing external databases from their Shiny applications. Although database access has long been possible using various database connector packages in R, it can be challenging to use them robustly in the dynamic environment that Shiny provides. So far, it has been mostly up to application authors to find the appropriate database drivers and to discover how to manage the database connections within an application. In order to demystify this process, we wrote a series of articles ([first one here](http://shiny.rstudio-staging.com/articles/overview.html)) that covers the basics of connecting to an external database, as well as some security precautions to keep in mind (e.g. [how to avoid SQL injection attacks](http://shiny.rstudio-staging.com/articles/sql-injections.html)).
Many Shiny users have asked about best practices for accessing external databases from their Shiny applications. Although database access has long been possible using various database connector packages in R, it can be challenging to use them robustly in the dynamic environment that Shiny provides. So far, it has been mostly up to application authors to find the appropriate database drivers and to discover how to manage the database connections within an application. In order to demystify this process, we wrote a series of articles ([first one here](http://shiny.rstudio.com/articles/overview.html)) that covers the basics of connecting to an external database, as well as some security precautions to keep in mind (e.g. [how to avoid SQL injection attacks](http://shiny.rstudio.com/articles/sql-injections.html)).
There are a few packages that you should look at if you're using a relational database in a Shiny app: the `dplyr` and `DBI` packages (both featured in the article linked to above), and the brand new `pool` package, which provides a further layer of abstraction to make it easier and safer to use either `DBI` or `dplyr`. `pool` is not yet on CRAN. In particular, `pool` will take care of managing connections, preventing memory leaks, and ensuring the best performance. See this [`pool` basics article](http://shiny.rstudio-staging.com/articles/pool-basics.html) and the [more advanced-level article](http://shiny.rstudio-staging.com/articles/pool-advanced.html) if you're feeling adventurous! (Both of these articles contain Shiny app examples that use `DBI` to connect to an external MySQL database.) If you are more comfortable with `dplyr` than `DBI`, don't miss the article about the [integration of `pool` and `dplyr`](http://shiny.rstudio-staging.com/articles/pool-dplyr.html).
There are a few packages that you should look at if you're using a relational database in a Shiny app: the `dplyr` and `DBI` packages (both featured in the article linked to above), and the brand new `pool` package, which provides a further layer of abstraction to make it easier and safer to use either `DBI` or `dplyr`. `pool` is not yet on CRAN. In particular, `pool` will take care of managing connections, preventing memory leaks, and ensuring the best performance. See this [`pool` basics article](http://shiny.rstudio.com/articles/pool-basics.html) and the [more advanced-level article](http://shiny.rstudio.com/articles/pool-advanced.html) if you're feeling adventurous! (Both of these articles contain Shiny app examples that use `DBI` to connect to an external MySQL database.) If you are more comfortable with `dplyr` than `DBI`, don't miss the article about the [integration of `pool` and `dplyr`](http://shiny.rstudio.com/articles/pool-dplyr.html).
If you're new to databases in the Shiny world, we recommend using `dplyr` and `pool` if possible. If you need greater control than `dplyr` offers (for example, if you need to modify data in the database or use transactions), then use `DBI` and `pool`. The `pool` package was introduced to make your life easier, but in no way constrains you, so we don't envision any situation in which you'd be better off *not* using it. The only caveat is that `pool` is not yet on CRAN, so you may prefer to wait for that.
@@ -65,14 +501,14 @@ If you're new to databases in the Shiny world, we recommend using `dplyr` and `p
There are many more minor features, small improvements, and bug fixes than we can cover here, so we'll just mention a few of the more noteworthy ones (the full changelog, with links to all the relevant issues and pull requests, is right below this section):
* **Error Sanitization**: you now have the option to sanitize error messages; in other words, the content of the original error message can be suppressed so that it doesn't leak any sensitive information. To sanitize errors everywhere in your app, just add `options(shiny.sanitize.errors = TRUE)` somewhere in your app. Read [this article](http://shiny.rstudio-staging.com/articles/sanitize-errors.html) for more, or play with the [demo app](https://gallery.shinyapps.io/110-error-sanitization/).
* **Error Sanitization**: you now have the option to sanitize error messages; in other words, the content of the original error message can be suppressed so that it doesn't leak any sensitive information. To sanitize errors everywhere in your app, just add `options(shiny.sanitize.errors = TRUE)` somewhere in your app. Read [this article](http://shiny.rstudio.com/articles/sanitize-errors.html) for more, or play with the [demo app](https://gallery.shinyapps.io/110-error-sanitization/).
* **Code Diagnostics**: if there is an error parsing `ui.R`, `server.R`, `app.R`, or `global.R`, Shiny will search the code for missing commas, extra commas, and unmatched braces, parens, and brackets, and will print out messages pointing out those problems. ([#1126](https://github.com/rstudio/shiny/pull/1126))
* **Reactlog visualization**: by default, the [`showReactLog()` function](http://shiny.rstudio-staging.com/reference/shiny/latest/showReactLog.html) (which brings up the reactive graph) also displays the time that each reactive and observer were active for:
* **Reactlog visualization**: by default, the [`showReactLog()` function](http://shiny.rstudio.com/reference/shiny/latest/showReactLog.html) (which brings up the reactive graph) also displays the time that each reactive and observer were active for:
<p align="center">
<img src="http://shiny.rstudio-staging.com/images/reactlog.png" alt="modal-dialog" width="75%"/>
<img src="http://shiny.rstudio.com/images/reactlog.png" alt="modal-dialog" width="75%"/>
</p>
This new feature can be turned off with `showReactLog(time = FALSE)`. This may be convenient if you have a large graph and don't want to have this new information cluttering it up. The elapsed time info shows up above each relevant node's label, and the time is also coded by color: the slowest reactive will be dark red and the fastest will be light red.
@@ -82,10 +518,10 @@ There are many more minor features, small improvements, and bug fixes than we ca
* **Nicer-looking tables**: we've made tables generated with `renderTable()` look cleaner and more modern. While this won't break any older code, the finished look of your table will be quite a bit different, as the following image shows:
<p align="center">
<img src="http://shiny.rstudio-staging.com/images/render-table.png" alt="render-table" width="75%"/>
<img src="http://shiny.rstudio.com/images/render-table.png" alt="render-table" width="75%"/>
</p>
For more, read our [short article](http://shiny.rstudio-staging.com/articles/render-table.html) about this update, experiment with all the new features in this [demo app](https://gallery.shinyapps.io/109-render-table/), or check out the [reference documentation](http://shiny.rstudio-staging.com/reference/shiny/latest/renderTable.html).
For more, read our [short article](http://shiny.rstudio.com/articles/render-table.html) about this update, experiment with all the new features in this [demo app](https://gallery.shinyapps.io/109-render-table/), or check out the [reference documentation](http://shiny.rstudio.com/reference/shiny/latest/renderTable.html).
## Full changelog
@@ -301,7 +737,7 @@ shiny 0.12.1
shiny 0.12.0
============
In addition to the changes listed below (in the *Full Changelog* section), there is an infrastructure change that could affect existing Shiny apps.
In addition to the changes listed below (in the *Full Changelog* section), there is an infrastructure change that could affect existing Shiny apps.
### JSON serialization
@@ -392,13 +828,13 @@ Shiny 0.11 switches away from the Bootstrap 2 web framework to the next version,
### Known issues for migration
* In Bootstrap 3, images in `<img>` tags are no longer automatically scaled to the width of their container. If you use `img()` in your UI code, or `<img>` tags in your raw HTML source, it's possible that they will be too large in the new version of Shiny. To address this you can add the `img-responsive` class:
```r
img(src = "picture.png", class = "img-responsive")
```
The R code above will generate the following HTML:
```html
<img src="picture.png" class="img-responsive">
```

33
R/app.R
View File

@@ -20,9 +20,11 @@
#' @param onStart A function that will be called before the app is actually run.
#' This is only needed for \code{shinyAppObj}, since in the \code{shinyAppDir}
#' case, a \code{global.R} file can be used for this purpose.
#' @param options Named options that should be passed to the `runApp` call. You
#' can also specify \code{width} and \code{height} parameters which provide a
#' hint to the embedding environment about the ideal height/width for the app.
#' @param options Named options that should be passed to the \code{runApp} call
#' (these can be any of the following: "port", "launch.browser", "host", "quiet",
#' "display.mode" and "test.mode"). You can also specify \code{width} and
#' \code{height} parameters which provide a hint to the embedding environment
#' about the ideal height/width for the app.
#' @param uiPattern A regular expression that will be applied to each \code{GET}
#' request to determine whether the \code{ui} should be used to handle the
#' request. Note that the entire request path must match the regular
@@ -39,6 +41,8 @@
#' @examples
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#' options(device.ask.default = FALSE)
#'
#' shinyApp(
#' ui = fluidPage(
#' numericInput("n", "n", 1),
@@ -67,7 +71,7 @@
#' }
#' @export
shinyApp <- function(ui=NULL, server=NULL, onStart=NULL, options=list(),
uiPattern="/", enableBookmarking = NULL) {
uiPattern="/", enableBookmarking=NULL) {
if (is.null(server)) {
stop("`server` missing from shinyApp")
}
@@ -208,7 +212,7 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
if (file.exists(file.path.ci(appDir, "global.R")))
sourceUTF8(file.path.ci(appDir, "global.R"))
}
onEnd <- function() {
onStop <- function() {
setwd(oldwd)
monitorHandle()
monitorHandle <<- NULL
@@ -219,7 +223,7 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
httpHandler = joinHandlers(c(uiHandler, wwwDir, fallbackWWWDir)),
serverFuncSource = serverFuncSource,
onStart = onStart,
onEnd = onEnd,
onStop = onStop,
options = options
),
class = "shiny.appobj"
@@ -231,13 +235,13 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
# ignored when checking extensions. If any changes are detected, all connected
# Shiny sessions are reloaded.
#
# Use option(shiny.autoreload = TRUE) to enable this behavior. Since monitoring
# Use options(shiny.autoreload = TRUE) to enable this behavior. Since monitoring
# for changes is expensive (we are polling for mtimes here, nothing fancy) this
# feature is intended only for development.
#
# You can customize the file patterns Shiny will monitor by setting the
# shiny.autoreload.pattern option. For example, to monitor only ui.R:
# option(shiny.autoreload.pattern = glob2rx("ui.R"))
# options(shiny.autoreload.pattern = glob2rx("ui.R"))
#
# The return value is a function that halts monitoring when called.
initAutoReloadMonitor <- function(dir) {
@@ -313,8 +317,9 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
oldwd <<- getwd()
setwd(appDir)
monitorHandle <<- initAutoReloadMonitor(appDir)
if (!is.null(appObj()$onStart)) appObj()$onStart()
}
onEnd <- function() {
onStop <- function() {
setwd(oldwd)
monitorHandle()
monitorHandle <<- NULL
@@ -325,7 +330,7 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
httpHandler = joinHandlers(c(dynHttpHandler, wwwDir, fallbackWWWDir)),
serverFuncSource = dynServerFuncSource,
onStart = onStart,
onEnd = onEnd,
onStop = onStop,
options = options
),
class = "shiny.appobj"
@@ -373,11 +378,13 @@ is.shiny.appobj <- function(x) {
print.shiny.appobj <- function(x, ...) {
opts <- x$options %OR% list()
opts <- opts[names(opts) %in%
c("port", "launch.browser", "host", "quiet", "display.mode")]
c("port", "launch.browser", "host", "quiet",
"display.mode", "test.mode")]
args <- c(list(x), opts)
# Quote x and put runApp in quotes so that there's a nicer stack trace (#1851)
args <- c(list(quote(x)), opts)
do.call(runApp, args)
do.call("runApp", args)
}
#' @rdname shinyApp

View File

@@ -349,7 +349,7 @@ RestoreContext <- R6Class("RestoreContext",
mapply(names(vals), vals, SIMPLIFY = FALSE,
FUN = function(name, value) {
tryCatch(
jsonlite::fromJSON(value),
safeFromJSON(value),
error = function(e) {
stop("Failed to parse URL parameter \"", name, "\"")
}
@@ -362,7 +362,7 @@ RestoreContext <- R6Class("RestoreContext",
self$input <- RestoreInputSet$new(inputs)
values <- valuesFromJSON(values)
self$values <- list2env(values, self$values)
self$values <- list2env2(values, self$values)
}
)
)
@@ -385,7 +385,7 @@ RestoreInputSet <- R6Class("RestoreInputSet",
public = list(
initialize = function(values) {
private$values <- list2env(values, parent = emptyenv())
private$values <- list2env2(values, parent = emptyenv())
},
exists = function(name) {
@@ -448,14 +448,30 @@ withRestoreContext <- function(ctx, expr) {
# Is there a current restore context?
hasCurrentRestoreContext <- function() {
restoreCtxStack$size() > 0
if (restoreCtxStack$size() > 0)
return(TRUE)
domain <- getDefaultReactiveDomain()
if (!is.null(domain) && !is.null(domain$restoreContext))
return(TRUE)
return(FALSE)
}
# Call to access the current restore context
# Call to access the current restore context. First look on the restore
# context stack, and if not found, then see if there's one on the current
# reactive domain. In practice, the only time there will be a restore context
# on the stack is when executing the UI function; when executing server code,
# the restore context will be attached to the domain/session.
getCurrentRestoreContext <- function() {
ctx <- restoreCtxStack$peek()
if (is.null(ctx)) {
stop("No restore context found")
domain <- getDefaultReactiveDomain()
if (is.null(domain) || is.null(domain$restoreContext)) {
stop("No restore context found")
}
ctx <- domain$restoreContext
}
ctx
}
@@ -493,12 +509,90 @@ restoreInput <- function(id, default) {
#' It typically is called from an observer. Note that this will not work in
#' Internet Explorer 9 and below.
#'
#' For \code{mode = "push"}, only three updates are currently allowed:
#' \enumerate{
#' \item the query string (format: \code{?param1=val1&param2=val2})
#' \item the hash (format: \code{#hash})
#' \item both the query string and the hash
#' (format: \code{?param1=val1&param2=val2#hash})
#' }
#'
#' In other words, if \code{mode = "push"}, the \code{queryString} must start
#' with either \code{?} or with \code{#}.
#'
#' A technical curiosity: under the hood, this function is calling the HTML5
#' history API (which is where the names for the \code{mode} argument come from).
#' When \code{mode = "replace"}, the function called is
#' \code{window.history.replaceState(null, null, queryString)}.
#' When \code{mode = "push"}, the function called is
#' \code{window.history.pushState(null, null, queryString)}.
#'
#' @param queryString The new query string to show in the location bar.
#' @param mode When the query string is updated, should the the current history
#' entry be replaced (default), or should a new history entry be pushed onto
#' the history stack? The former should only be used in a live bookmarking
#' context. The latter is useful if you want to navigate between states using
#' the browser's back and forward buttons. See Examples.
#' @param session A Shiny session object.
#' @seealso \code{\link{enableBookmarking}} for examples.
#' @seealso \code{\link{enableBookmarking}}, \code{\link{getQueryString}}
#' @examples
#' ## Only run these examples in interactive sessions
#' if (interactive()) {
#'
#' ## App 1: Doing "live" bookmarking
#' ## Update the browser's location bar every time an input changes.
#' ## This should not be used with enableBookmarking("server"),
#' ## because that would create a new saved state on disk every time
#' ## the user changes an input.
#' enableBookmarking("url")
#' shinyApp(
#' ui = function(req) {
#' fluidPage(
#' textInput("txt", "Text"),
#' checkboxInput("chk", "Checkbox")
#' )
#' },
#' server = function(input, output, session) {
#' observe({
#' # Trigger this observer every time an input changes
#' reactiveValuesToList(input)
#' session$doBookmark()
#' })
#' onBookmarked(function(url) {
#' updateQueryString(url)
#' })
#' }
#' )
#'
#' ## App 2: Printing the value of the query string
#' ## (Use the back and forward buttons to see how the browser
#' ## keeps a record of each state)
#' shinyApp(
#' ui = fluidPage(
#' textInput("txt", "Enter new query string"),
#' helpText("Format: ?param1=val1&param2=val2"),
#' actionButton("go", "Update"),
#' hr(),
#' verbatimTextOutput("query")
#' ),
#' server = function(input, output, session) {
#' observeEvent(input$go, {
#' updateQueryString(input$txt, mode = "push")
#' })
#' output$query <- renderText({
#' query <- getQueryString()
#' queryText <- paste(names(query), query,
#' sep = "=", collapse=", ")
#' paste("Your query string is:\n", queryText)
#' })
#' }
#' )
#' }
#' @export
updateQueryString <- function(queryString, session = getDefaultReactiveDomain()) {
session$updateQueryString(queryString)
updateQueryString <- function(queryString, mode = c("replace", "push"),
session = getDefaultReactiveDomain()) {
mode <- match.arg(mode)
session$updateQueryString(queryString, mode)
}
#' Create a button for bookmarking/sharing

View File

@@ -277,6 +277,7 @@ titlePanel <- function(title, windowTitle=title) {
#' @examples
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
#' options(device.ask.default = FALSE)
#'
#' # Define UI
#' ui <- fluidPage(
@@ -442,6 +443,7 @@ inputPanel <- function(...) {
#' @examples
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
#' options(device.ask.default = FALSE)
#'
#' # Server code used for all examples
#' server <- function(input, output) {
@@ -586,7 +588,7 @@ flexfill <- function(..., direction, flex, width = width, height = height) {
}
if (length(flex) > length(children)) {
flex <- flex[1:length(children)]
flex <- flex[seq_along(children)]
}
# The dimension along the main axis

View File

@@ -285,7 +285,8 @@ pageWithSidebar <- function(headerPanel,
#' example below).
#'
#' @seealso \code{\link{tabPanel}}, \code{\link{tabsetPanel}},
#' \code{\link{updateNavbarPage}}
#' \code{\link{updateNavbarPage}}, \code{\link{insertTab}},
#' \code{\link{showTab}}
#'
#' @examples
#' navbarPage("App Title",
@@ -342,10 +343,18 @@ navbarPage <- function(title,
tabs <- list(...)
tabset <- buildTabset(tabs, "nav navbar-nav", NULL, id, selected)
# function to return plain or fluid class name
className <- function(name) {
if (fluid)
paste(name, "-fluid", sep="")
else
name
}
# built the container div dynamically to support optional collapsibility
if (collapsible) {
navId <- paste("navbar-collapse-", p_randomInt(1000, 10000), sep="")
containerDiv <- div(class="container",
containerDiv <- div(class=className("container"),
div(class="navbar-header",
tags$button(type="button", class="navbar-toggle collapsed",
`data-toggle`="collapse", `data-target`=paste0("#", navId),
@@ -359,7 +368,7 @@ navbarPage <- function(title,
div(class="navbar-collapse collapse", id=navId, tabset$navList)
)
} else {
containerDiv <- div(class="container",
containerDiv <- div(class=className("container"),
div(class="navbar-header",
span(class="navbar-brand", pageTitle)
),
@@ -367,14 +376,6 @@ navbarPage <- function(title,
)
}
# function to return plain or fluid class name
className <- function(name) {
if (fluid)
paste(name, "-fluid", sep="")
else
name
}
# build the main tab content div
contentDiv <- div(class=className("container"))
if (!is.null(header))
@@ -393,10 +394,15 @@ navbarPage <- function(title,
)
}
#' @param menuName A name that identifies this \code{navbarMenu}. This
#' is needed if you want to insert/remove or show/hide an entire
#' \code{navbarMenu}.
#'
#' @rdname navbarPage
#' @export
navbarMenu <- function(title, ..., icon = NULL) {
navbarMenu <- function(title, ..., menuName = title, icon = NULL) {
structure(list(title = title,
menuName = menuName,
tabs = list(...),
iconClass = iconClass(icon)),
class = "shiny.navbarmenu")
@@ -502,6 +508,8 @@ mainPanel <- function(..., width = 8) {
#'
#' @param condition A JavaScript expression that will be evaluated repeatedly to
#' determine whether the panel should be displayed.
#' @param ns The \code{\link[=NS]{namespace}} object of the current module, if
#' any.
#' @param ... Elements to include in the panel.
#'
#' @note You are not recommended to use special JavaScript characters such as a
@@ -510,32 +518,55 @@ mainPanel <- function(..., width = 8) {
#' \code{input["foo.bar"]} instead of \code{input.foo.bar} to read the input
#' value.
#' @examples
#' sidebarPanel(
#' selectInput(
#' "plotType", "Plot Type",
#' c(Scatter = "scatter",
#' Histogram = "hist")),
#'
#' # Only show this panel if the plot type is a histogram
#' conditionalPanel(
#' condition = "input.plotType == 'hist'",
#' selectInput(
#' "breaks", "Breaks",
#' c("Sturges",
#' "Scott",
#' "Freedman-Diaconis",
#' "[Custom]" = "custom")),
#'
#' # Only show this panel if Custom is selected
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#' ui <- fluidPage(
#' sidebarPanel(
#' selectInput("plotType", "Plot Type",
#' c(Scatter = "scatter", Histogram = "hist")
#' ),
#' # Only show this panel if the plot type is a histogram
#' conditionalPanel(
#' condition = "input.breaks == 'custom'",
#' sliderInput("breakCount", "Break Count", min=1, max=1000, value=10)
#' condition = "input.plotType == 'hist'",
#' selectInput(
#' "breaks", "Breaks",
#' c("Sturges", "Scott", "Freedman-Diaconis", "[Custom]" = "custom")
#' ),
#' # Only show this panel if Custom is selected
#' conditionalPanel(
#' condition = "input.breaks == 'custom'",
#' sliderInput("breakCount", "Break Count", min = 1, max = 50, value = 10)
#' )
#' )
#' )
#' )
#' ),
#' mainPanel(
#' plotOutput("plot")
#' )
#' )
#'
#' server <- function(input, output) {
#' x <- rnorm(100)
#' y <- rnorm(100)
#'
#' output$plot <- renderPlot({
#' if (input$plotType == "scatter") {
#' plot(x, y)
#' } else {
#' breaks <- input$breaks
#' if (breaks == "custom") {
#' breaks <- input$breakCount
#' }
#'
#' hist(x, breaks = breaks)
#' }
#' })
#' }
#'
#' shinyApp(ui, server)
#' }
#' @export
conditionalPanel <- function(condition, ...) {
div('data-display-if'=condition, ...)
conditionalPanel <- function(condition, ..., ns = NS(NULL)) {
div(`data-display-if`=condition, `data-ns-prefix`=ns(""), ...)
}
#' Create a help text element
@@ -609,7 +640,8 @@ tabPanel <- function(title, ..., value = title, icon = NULL) {
#' Bootstrap 3.
#' @return A tabset that can be passed to \code{\link{mainPanel}}
#'
#' @seealso \code{\link{tabPanel}}, \code{\link{updateTabsetPanel}}
#' @seealso \code{\link{tabPanel}}, \code{\link{updateTabsetPanel}},
#' \code{\link{insertTab}}, \code{\link{showTab}}
#'
#' @examples
#' # Show a tabset that includes a plot, summary, and
@@ -676,7 +708,9 @@ tabsetPanel <- function(...,
#' supported. This is because version 0.11 switched to Bootstrap 3, which
#' doesn't support separators.
#'
#' @seealso \code{\link{tabPanel}}, \code{\link{updateNavlistPanel}}
#' @seealso \code{\link{tabPanel}}, \code{\link{updateNavlistPanel}},
#' \code{\link{insertTab}}, \code{\link{showTab}}
#'
#' @examples
#' fluidPage(
#'
@@ -726,189 +760,158 @@ navlistPanel <- function(...,
fixedRow(columns)
}
# Helpers to build tabsetPanels (& Co.) and their elements
markTabAsSelected <- function(x) {
attr(x, "selected") <- TRUE
x
}
buildTabset <- function(tabs, ulClass, textFilter = NULL,
id = NULL, selected = NULL) {
isTabSelected <- function(x) {
isTRUE(attr(x, "selected", exact = TRUE))
}
# This function proceeds in two phases. First, it scans over all the items
# to find and mark which tab should start selected. Then it actually builds
# the tab nav and tab content lists.
containsSelectedTab <- function(tabs) {
any(vapply(tabs, isTabSelected, logical(1)))
}
# Mark an item as selected
markSelected <- function(x) {
attr(x, "selected") <- TRUE
x
}
findAndMarkSelectedTab <- function(tabs, selected, foundSelected) {
tabs <- lapply(tabs, function(div) {
if (foundSelected || is.character(div)) {
# Strings are not selectable items
# Returns TRUE if an item is selected
isSelected <- function(x) {
isTRUE(attr(x, "selected", exact = TRUE))
}
# Returns TRUE if a list of tab items contains a selected tab, FALSE
# otherwise.
containsSelected <- function(tabs) {
any(vapply(tabs, isSelected, logical(1)))
}
# Take a pass over all tabs, and mark the selected tab.
foundSelectedItem <- FALSE
findAndMarkSelected <- function(tabs, selected) {
lapply(tabs, function(divTag) {
if (foundSelectedItem) {
# If we already found the selected tab, no need to keep looking
} else if (is.character(divTag)) {
# Strings don't represent selectable items
} else if (inherits(divTag, "shiny.navbarmenu")) {
# Navbar menu
divTag$tabs <- findAndMarkSelected(divTag$tabs, selected)
} else if (inherits(div, "shiny.navbarmenu")) {
# Recur for navbarMenus
res <- findAndMarkSelectedTab(div$tabs, selected, foundSelected)
div$tabs <- res$tabs
foundSelected <<- res$foundSelected
} else {
# Base case: regular tab item. If the `selected` argument is
# provided, check for a match in the existing tabs; else,
# mark first available item as selected
if (is.null(selected)) {
foundSelected <<- TRUE
div <- markTabAsSelected(div)
} else {
# Regular tab item
if (is.null(selected)) {
# If selected tab isn't specified, mark first available item
# as selected.
foundSelectedItem <<- TRUE
divTag <- markSelected(divTag)
} else {
# If selected tab is specified, check for a match
tabValue <- divTag$attribs$`data-value` %OR% divTag$attribs$title
if (identical(selected, tabValue)) {
foundSelectedItem <<- TRUE
divTag <- markSelected(divTag)
}
tabValue <- div$attribs$`data-value` %OR% div$attribs$title
if (identical(selected, tabValue)) {
foundSelected <<- TRUE
div <- markTabAsSelected(div)
}
}
}
return(div)
})
return(list(tabs = tabs, foundSelected = foundSelected))
}
return(divTag)
})
}
# Append an optional icon to an aTag
appendIcon <- function(aTag, iconClass) {
if (!is.null(iconClass)) {
# Returns the icon object (or NULL if none), provided either a
# tabPanel, OR the icon class
getIcon <- function(tab = NULL, iconClass = NULL) {
if (!is.null(tab)) iconClass <- tab$attribs$`data-icon-class`
if (!is.null(iconClass)) {
if (grepl("fa-", iconClass, fixed = TRUE)) {
# for font-awesome we specify fixed-width
if (grepl("fa-", iconClass, fixed = TRUE))
iconClass <- paste(iconClass, "fa-fw")
aTag <- tagAppendChild(aTag, icon(name = NULL, class = iconClass))
iconClass <- paste(iconClass, "fa-fw")
}
aTag
icon(name = NULL, class = iconClass)
} else NULL
}
# Text filter for navbarMenu's (plain text) separators
navbarMenuTextFilter <- function(text) {
if (grepl("^\\-+$", text)) tags$li(class = "divider")
else tags$li(class = "dropdown-header", text)
}
# This function is called internally by navbarPage, tabsetPanel
# and navlistPanel
buildTabset <- function(tabs, ulClass, textFilter = NULL, id = NULL,
selected = NULL, foundSelected = FALSE) {
res <- findAndMarkSelectedTab(tabs, selected, foundSelected)
tabs <- res$tabs
foundSelected <- res$foundSelected
# add input class if we have an id
if (!is.null(id)) ulClass <- paste(ulClass, "shiny-tab-input")
if (anyNamed(tabs)) {
nms <- names(tabs)
nms <- nms[nzchar(nms)]
stop("Tabs should all be unnamed arguments, but some are named: ",
paste(nms, collapse = ", "))
}
# Build the tabset
build <- function(tabs, ulClass, textFilter = NULL, id = NULL) {
# add tab input sentinel class if we have an id
if (!is.null(id))
ulClass <- paste(ulClass, "shiny-tab-input")
tabsetId <- p_randomInt(1000, 10000)
tabs <- lapply(seq_len(length(tabs)), buildTabItem,
tabsetId = tabsetId, foundSelected = foundSelected,
tabs = tabs, textFilter = textFilter)
if (anyNamed(tabs)) {
nms <- names(tabs)
nms <- nms[nzchar(nms)]
stop("Tabs should all be unnamed arguments, but some are named: ",
paste(nms, collapse = ", "))
tabNavList <- tags$ul(class = ulClass, id = id,
`data-tabsetid` = tabsetId, lapply(tabs, "[[", 1))
tabContent <- tags$div(class = "tab-content",
`data-tabsetid` = tabsetId, lapply(tabs, "[[", 2))
list(navList = tabNavList, content = tabContent)
}
# Builds tabPanel/navbarMenu items (this function used to be
# declared inside the buildTabset() function and it's been
# refactored for clarity and reusability). Called internally
# by buildTabset.
buildTabItem <- function(index, tabsetId, foundSelected, tabs = NULL,
divTag = NULL, textFilter = NULL) {
divTag <- if (!is.null(divTag)) divTag else tabs[[index]]
if (is.character(divTag) && !is.null(textFilter)) {
# text item: pass it to the textFilter if it exists
liTag <- textFilter(divTag)
divTag <- NULL
} else if (inherits(divTag, "shiny.navbarmenu")) {
# navbarMenu item: build the child tabset
tabset <- buildTabset(divTag$tabs, "dropdown-menu",
navbarMenuTextFilter, foundSelected = foundSelected)
# if this navbarMenu contains a selected item, mark it active
containsSelected <- containsSelectedTab(divTag$tabs)
liTag <- tags$li(
class = paste0("dropdown", if (containsSelected) " active"),
tags$a(href = "#",
class = "dropdown-toggle", `data-toggle` = "dropdown",
`data-value` = divTag$menuName,
getIcon(iconClass = divTag$iconClass),
divTag$title, tags$b(class = "caret")
),
tabset$navList # inner tabPanels items
)
# list of tab content divs from the child tabset
divTag <- tabset$content$children
} else {
# tabPanel item: create the tab's liTag and divTag
tabId <- paste("tab", tabsetId, index, sep = "-")
liTag <- tags$li(
tags$a(
href = paste("#", tabId, sep = ""),
`data-toggle` = "tab",
`data-value` = divTag$attribs$`data-value`,
getIcon(iconClass = divTag$attribs$`data-icon-class`),
divTag$attribs$title
)
)
# if this tabPanel is selected item, mark it active
if (isTabSelected(divTag)) {
liTag$attribs$class <- "active"
divTag$attribs$class <- "tab-pane active"
}
tabNavList <- tags$ul(class = ulClass, id = id)
tabContent <- tags$div(class = "tab-content")
tabsetId <- p_randomInt(1000, 10000)
tabId <- 1
buildItem <- function(divTag) {
# check for text; pass it to the textFilter or skip it if there is none
if (is.character(divTag)) {
if (!is.null(textFilter)) {
tabNavList <<- tagAppendChild(tabNavList, textFilter(divTag))
}
} else if (inherits(divTag, "shiny.navbarmenu")) {
# create the a tag
aTag <- tags$a(href="#",
class="dropdown-toggle",
`data-toggle`="dropdown")
# add optional icon
aTag <- appendIcon(aTag, divTag$iconClass)
# add the title and caret
aTag <- tagAppendChild(aTag, divTag$title)
aTag <- tagAppendChild(aTag, tags$b(class="caret"))
# build the dropdown list element
liTag <- tags$li(class = "dropdown", aTag)
# text filter for separators
textFilter <- function(text) {
if (grepl("^\\-+$", text))
tags$li(class="divider")
else
tags$li(class="dropdown-header", text)
}
# build the child tabset
tabset <- build(divTag$tabs, "dropdown-menu", textFilter)
liTag <- tagAppendChild(liTag, tabset$navList)
# If this navbar menu contains a selected item, mark it as active
if (containsSelected(divTag$tabs)) {
liTag$attribs$class <- paste(liTag$attribs$class, "active")
}
tabNavList <<- tagAppendChild(tabNavList, liTag)
# don't add a standard tab content div, rather add the list of tab
# content divs that are contained within the tabset
tabContent <<- tagAppendChildren(tabContent,
list = tabset$content$children)
} else {
# Standard navbar item
# compute id and assign it to the div
thisId <- paste("tab", tabsetId, tabId, sep="-")
divTag$attribs$id <- thisId
tabId <<- tabId + 1
tabValue <- divTag$attribs$`data-value`
# create the a tag
aTag <- tags$a(href=paste("#", thisId, sep=""),
`data-toggle` = "tab",
`data-value` = tabValue)
# append optional icon
aTag <- appendIcon(aTag, divTag$attribs$`data-icon-class`)
# add the title
aTag <- tagAppendChild(aTag, divTag$attribs$title)
# create the li tag
liTag <- tags$li(aTag)
# If selected, set appropriate classes on li tag and div tag.
if (isSelected(divTag)) {
liTag$attribs$class <- "active"
divTag$attribs$class <- "tab-pane active"
}
divTag$attribs$title <- NULL
# append the elements to our lists
tabNavList <<- tagAppendChild(tabNavList, liTag)
tabContent <<- tagAppendChild(tabContent, divTag)
}
}
lapply(tabs, buildItem)
list(navList = tabNavList, content = tabContent)
divTag$attribs$id <- tabId
divTag$attribs$title <- NULL
}
# Finally, actually invoke the functions to do the processing.
tabs <- findAndMarkSelected(tabs, selected)
build(tabs, ulClass, textFilter, id)
return(list(liTag = liTag, divTag = divTag))
}
@@ -935,21 +938,34 @@ textOutput <- function(outputId, container = if (inline) span else div, inline =
#' Render a reactive output variable as verbatim text within an
#' application page. The text will be included within an HTML \code{pre} tag.
#' @param outputId output variable to read the value from
#' @param placeholder if the output is empty or \code{NULL}, should an empty
#' rectangle be displayed to serve as a placeholder? (does not affect
#' behavior when the the output in nonempty)
#' @return A verbatim text output element that can be included in a panel
#' @details Text is HTML-escaped prior to rendering. This element is often used
#' with the \link{renderPrint} function to preserve fixed-width formatting
#' of printed objects.
#' with the \link{renderPrint} function to preserve fixed-width formatting
#' of printed objects.
#' @examples
#' mainPanel(
#' h4("Summary"),
#' verbatimTextOutput("summary"),
#'
#' h4("Observations"),
#' tableOutput("view")
#' )
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#' shinyApp(
#' ui = basicPage(
#' textInput("txt", "Enter the text to display below:"),
#' verbatimTextOutput("default"),
#' verbatimTextOutput("placeholder", placeholder = TRUE)
#' ),
#' server = function(input, output) {
#' output$default <- renderText({ input$txt })
#' output$placeholder <- renderText({ input$txt })
#' }
#' )
#' }
#' @export
verbatimTextOutput <- function(outputId) {
textOutput(outputId, container = pre)
verbatimTextOutput <- function(outputId, placeholder = FALSE) {
pre(id = outputId,
class = paste(c("shiny-text-output", if (!placeholder) "noplaceholder"),
collapse = " ")
)
}
@@ -1123,7 +1139,7 @@ imageOutput <- function(outputId, width = "100%", height="400px",
#' same \code{id} to disappear.
#' @inheritParams textOutput
#' @note The arguments \code{clickId} and \code{hoverId} only work for R base
#' graphics (see the \pkg{\link{graphics}} package). They do not work for
#' graphics (see the \pkg{\link[graphics:graphics-package]{graphics}} package). They do not work for
#' \pkg{\link[grid:grid-package]{grid}}-based graphics, such as \pkg{ggplot2},
#' \pkg{lattice}, and so on.
#'
@@ -1421,6 +1437,7 @@ 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 ... Other arguments to pass to the container tag function.
#'
#' @examples
#' \dontrun{
@@ -1439,27 +1456,29 @@ uiOutput <- htmlOutput
#' }
#'
#' @aliases downloadLink
#' @seealso downloadHandler
#' @seealso \code{\link{downloadHandler}}
#' @export
downloadButton <- function(outputId,
label="Download",
class=NULL) {
class=NULL, ...) {
aTag <- tags$a(id=outputId,
class=paste('btn btn-default shiny-download-link', class),
href='',
target='_blank',
download=NA,
icon("download"),
label)
label, ...)
}
#' @rdname downloadButton
#' @export
downloadLink <- function(outputId, label="Download", class=NULL) {
downloadLink <- function(outputId, label="Download", class=NULL, ...) {
tags$a(id=outputId,
class=paste(c('shiny-download-link', class), collapse=" "),
href='',
target='_blank',
label)
download=NA,
label, ...)
}
@@ -1470,8 +1489,9 @@ downloadLink <- function(outputId, label="Download", class=NULL) {
#' \code{\link{navbarPage}}.
#'
#' @param name Name of icon. Icons are drawn from the
#' \href{http://fontawesome.io/icons/}{Font Awesome} and
#' \href{http://getbootstrap.com/components/#glyphicons}{Glyphicons"}
#' \href{https://fontawesome.com/}{Font Awesome Free} (currently icons from
#' the v5.3.1 set are supported with the v4 naming convention) and
#' \href{http://getbootstrap.com/components/#glyphicons}{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")
@@ -1527,12 +1547,15 @@ icon <- function(name, class = NULL, lib = "font-awesome") {
# font-awesome needs an additional dependency (glyphicon is in bootstrap)
if (lib == "font-awesome") {
htmlDependencies(iconTag) <- htmlDependency(
"font-awesome", "4.6.3", c(href="shared/font-awesome"),
stylesheet = "css/font-awesome.min.css"
"font-awesome", "5.3.1", "www/shared/fontawesome", package = "shiny",
stylesheet = c(
"css/all.min.css",
"css/v4-shims.min.css"
)
)
}
iconTag
htmltools::browsable(iconTag)
}
# Helper funtion to extract the class from an icon

561
R/cache-disk.R Normal file
View File

@@ -0,0 +1,561 @@
#' 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 \code{get()}
#' and \code{set()} methods. Objects are automatically pruned from the cache
#' according to the parameters \code{max_size}, \code{max_age}, \code{max_n},
#' and \code{evict}.
#'
#'
#' @section Missing Keys:
#'
#' The \code{missing} and \code{exec_missing} parameters controls what happens
#' when \code{get()} is called with a key that is not in the cache (a cache
#' miss). The default behavior is to return a \code{\link{key_missing}}
#' object. This is a \emph{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 \code{\link{is.key_missing}} function. You can
#' also have \code{get()} return a different sentinel value, like \code{NULL}.
#' If you want to throw an error on a cache miss, you can do so by providing a
#' function for \code{missing} that takes one argument, the key, and also use
#' \code{exec_missing=TRUE}.
#'
#' When the cache is created, you can supply a value for \code{missing}, which
#' sets the default value to be returned for missing values. It can also be
#' overridden when \code{get()} is called, by supplying a \code{missing}
#' argument. For example, if you use \code{cache$get("mykey", missing =
#' NULL)}, it will return \code{NULL} if the key is not in the cache.
#'
#' If your cache is configured so that \code{get()} returns a sentinel value
#' to represent a cache miss, then \code{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 \code{get()} encounters missing
#' key. If the function returns a value, then \code{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 \code{get()} will not return a value.
#'
#' To do this, pass a one-argument function to \code{missing}, and use
#' \code{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 \code{get()} should be wrapped with
#' \code{\link{tryCatch}()} to gracefully handle missing keys.
#'
#' @section Cache pruning:
#'
#' Cache pruning occurs when \code{set()} is called, or it can be invoked
#' manually by calling \code{prune()}.
#'
#' The disk cache will throttle the pruning so that it does not happen on
#' every call to \code{set()}, because the filesystem operations for checking
#' the status of files can be slow. Instead, it will prune once in every 20
#' calls to \code{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
#' \code{max_age}, they will be removed.
#'
#' The \code{max_size} and \code{max_n} parameters are applied to the cache as
#' a whole, in contrast to \code{max_age}, which is applied to each object
#' individually.
#'
#' If the number of objects in the cache exceeds \code{max_n}, then objects
#' will be removed from the cache according to the eviction policy, which is
#' set with the \code{evict} parameter. Objects will be removed so that the
#' number of items is \code{max_n}.
#'
#' If the size of the objects in the cache exceeds \code{max_size}, then
#' objects will be removed from the cache. Objects will be removed from the
#' cache so that the total size remains under \code{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
#' \code{get()} is called. If the target object is older than \code{max_age},
#' it will be removed and the cache will report it as a missing value.
#'
#' @section Eviction policies:
#'
#' If \code{max_n} or \code{max_size} are used, then objects will be removed
#' from the cache according to an eviction policy. The available eviction
#' policies are:
#'
#' \describe{
#' \item{\code{"lru"}}{
#' Least Recently Used. The least recently used objects will be removed.
#' This uses the filesystem's mtime property. When "lru" is used, each
#' \code{get()} is called, it will update the file's mtime.
#' }
#' \item{\code{"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 \code{exists(key)} to check
#' if an object is in the cache, and then call \code{get(key)}, the object may
#' be removed from the cache in between those two calls, and \code{get(key)}
#' will throw an error. Instead of calling the two functions, it is better to
#' simply call \code{get(key)}, and use \code{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 \code{file.remove()} failing to remove a file that has
#' already been deleted.
#'
#'
#' @section Methods:
#'
#' A disk cache object has the following methods:
#'
#' \describe{
#' \item{\code{get(key, missing, exec_missing)}}{
#' Returns the value associated with \code{key}. If the key is not in the
#' cache, then it returns the value specified by \code{missing} or,
#' \code{missing} is a function and \code{exec_missing=TRUE}, then
#' executes \code{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{\code{set(key, value)}}{
#' Stores the \code{key}-\code{value} pair in the cache.
#' }
#' \item{\code{exists(key)}}{
#' Returns \code{TRUE} if the cache contains the key, otherwise
#' \code{FALSE}.
#' }
#' \item{\code{size()}}{
#' Returns the number of items currently in the cache.
#' }
#' \item{\code{keys()}}{
#' Returns a character vector of all keys currently in the cache.
#' }
#' \item{\code{reset()}}{
#' Clears all objects from the cache.
#' }
#' \item{\code{destroy()}}{
#' Clears all objects in the cache, and removes the cache directory from
#' disk.
#' }
#' \item{\code{prune()}}{
#' Prunes the cache, using the parameters specified by \code{max_size},
#' \code{max_age}, \code{max_n}, and \code{evict}.
#' }
#' }
#'
#' @param dir Directory to store files for the cache. If \code{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 \code{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
#' \code{evict}. Use \code{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 \code{evict}. Use \code{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, \code{"lru"} and \code{"fifo"} are
#' supported.
#' @param destroy_on_finalize If \code{TRUE}, then when the DiskCache object is
#' garbage collected, the cache directory and all objects inside of it will be
#' deleted from disk. If \code{FALSE} (the default), it will do nothing when
#' finalized.
#' @param missing A value to return or a function to execute when
#' \code{get(key)} is called but the key is not present in the cache. The
#' default is a \code{\link{key_missing}} object. If it is a function to
#' execute, the function must take one argument (the key), and you must also
#' use \code{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
#' \code{get()}. See section Missing keys for more information.
#' @param exec_missing If \code{FALSE} (the default), then treat \code{missing}
#' as a value to return when \code{get()} results in a cache miss. If
#' \code{TRUE}, treat \code{missing} as a function to execute when
#' \code{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 \code{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)
}
)
)

366
R/cache-memory.R Normal file
View File

@@ -0,0 +1,366 @@
#' 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 \code{get()} and
#' \code{set()} methods. Objects are automatically pruned from the cache
#' according to the parameters \code{max_size}, \code{max_age}, \code{max_n},
#' and \code{evict}.
#'
#' In a \code{MemoryCache}, R objects are stored directly in the cache; they are
#' not \emph{not} serialized before being stored in the cache. This contrasts
#' with other cache types, like \code{\link{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 \code{missing} and \code{exec_missing} parameters controls what happens
#' when \code{get()} is called with a key that is not in the cache (a cache
#' miss). The default behavior is to return a \code{\link{key_missing}}
#' object. This is a \emph{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 \code{\link{is.key_missing}} function. You can
#' also have \code{get()} return a different sentinel value, like \code{NULL}.
#' If you want to throw an error on a cache miss, you can do so by providing a
#' function for \code{missing} that takes one argument, the key, and also use
#' \code{exec_missing=TRUE}.
#'
#' When the cache is created, you can supply a value for \code{missing}, which
#' sets the default value to be returned for missing values. It can also be
#' overridden when \code{get()} is called, by supplying a \code{missing}
#' argument. For example, if you use \code{cache$get("mykey", missing =
#' NULL)}, it will return \code{NULL} if the key is not in the cache.
#'
#' If your cache is configured so that \code{get()} returns a sentinel value
#' to represent a cache miss, then \code{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 \code{get()} encounters missing
#' key. If the function returns a value, then \code{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 \code{get()} will not return a value.
#'
#' To do this, pass a one-argument function to \code{missing}, and use
#' \code{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 \code{get()} should be wrapped with
#' \code{\link{tryCatch}()} to gracefully handle missing keys.
#'
#' @section Cache pruning:
#'
#' Cache pruning occurs when \code{set()} is called, or it can be invoked
#' manually by calling \code{prune()}.
#'
#' When a pruning occurs, if there are any objects that are older than
#' \code{max_age}, they will be removed.
#'
#' The \code{max_size} and \code{max_n} parameters are applied to the cache as
#' a whole, in contrast to \code{max_age}, which is applied to each object
#' individually.
#'
#' If the number of objects in the cache exceeds \code{max_n}, then objects
#' will be removed from the cache according to the eviction policy, which is
#' set with the \code{evict} parameter. Objects will be removed so that the
#' number of items is \code{max_n}.
#'
#' If the size of the objects in the cache exceeds \code{max_size}, then
#' objects will be removed from the cache. Objects will be removed from the
#' cache so that the total size remains under \code{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
#' \code{get()} is called. If the target object is older than \code{max_age},
#' it will be removed and the cache will report it as a missing value.
#'
#' @section Eviction policies:
#'
#' If \code{max_n} or \code{max_size} are used, then objects will be removed
#' from the cache according to an eviction policy. The available eviction
#' policies are:
#'
#' \describe{
#' \item{\code{"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{\code{"fifo"}}{
#' First-in-first-out. The oldest objects will be removed.
#' }
#' }
#'
#' @section Methods:
#'
#' A disk cache object has the following methods:
#'
#' \describe{
#' \item{\code{get(key, missing, exec_missing)}}{
#' Returns the value associated with \code{key}. If the key is not in the
#' cache, then it returns the value specified by \code{missing} or,
#' \code{missing} is a function and \code{exec_missing=TRUE}, then
#' executes \code{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{\code{set(key, value)}}{
#' Stores the \code{key}-\code{value} pair in the cache.
#' }
#' \item{\code{exists(key)}}{
#' Returns \code{TRUE} if the cache contains the key, otherwise
#' \code{FALSE}.
#' }
#' \item{\code{size()}}{
#' Returns the number of items currently in the cache.
#' }
#' \item{\code{keys()}}{
#' Returns a character vector of all keys currently in the cache.
#' }
#' \item{\code{reset()}}{
#' Clears all objects from the cache.
#' }
#' \item{\code{destroy()}}{
#' Clears all objects in the cache, and removes the cache directory from
#' disk.
#' }
#' \item{\code{prune()}}{
#' Prunes the cache, using the parameters specified by \code{max_size},
#' \code{max_age}, \code{max_n}, and \code{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 <- new.env(parent = emptyenv())
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[[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[[key]] <- list(
key = key,
value = value,
size = size,
mtime = time,
atime = time
)
self$prune()
invisible(self)
},
exists = function(key) {
validate_key(key)
# Faster than `exists(key, envir = private$cache, inherits = FALSE)
!is.null(private$cache[[key]])
},
keys = function() {
ls(private$cache, sorted = FALSE) # Faster with sorted=FALSE
},
remove = function(key) {
private$log(paste0('remove: key "', key, '"'))
validate_key(key)
rm(list = key, envir = private$cache)
invisible(self)
},
reset = function() {
private$log(paste0('reset'))
rm(list = self$keys(), envir = private$cache)
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 = ", ")))
rm(list = info$key[rm_idx], envir = private$cache)
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 = ", ")))
rm(list = info$key[rm_idx], envir = private$cache)
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 = ", ")))
rm(list = info$key[rm_idx], envir = private$cache)
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[[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)
}
},
object_info = function() {
keys <- ls(private$cache, sorted = FALSE)
data.frame(
key = keys,
size = vapply(keys, function(key) private$cache[[key]]$size, 0),
mtime = vapply(keys, function(key) private$cache[[key]]$mtime, 0),
atime = vapply(keys, function(key) private$cache[[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)
}
)
)

33
R/cache-utils.R Normal file
View File

@@ -0,0 +1,33 @@
#' A Key Missing object
#'
#' A \code{key_missing} object represents a cache miss.
#'
#' @param x An object to test.
#'
#' @seealso \code{\link{diskCache}}, \code{\link{memoryCache}}.
#'
#' @export
key_missing <- function() {
structure(list(), class = "key_missing")
}
#' @rdname key_missing
#' @export
is.key_missing <- function(x) {
inherits(x, "key_missing")
}
#' @export
print.key_missing <- function(x, ...) {
cat("<Key Missing>\n")
}
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.")
}
}

View File

@@ -76,7 +76,7 @@ getCallNames <- function(calls) {
}
getLocs <- function(calls) {
sapply(calls, function(call) {
vapply(calls, function(call) {
srcref <- attr(call, "srcref", exact = TRUE)
if (!is.null(srcref)) {
srcfile <- attr(srcref, "srcfile", exact = TRUE)
@@ -86,7 +86,24 @@ getLocs <- function(calls) {
}
}
return("")
})
}, character(1))
}
getCallCategories <- function(calls) {
vapply(calls, function(call) {
srcref <- attr(call, "srcref", exact = TRUE)
if (!is.null(srcref)) {
srcfile <- attr(srcref, "srcfile", exact = TRUE)
if (!is.null(srcfile)) {
if (!is.null(srcfile$original)) {
return("pkg")
} else {
return("user")
}
}
}
return("")
}, character(1))
}
#' @details \code{captureStackTraces} runs the given \code{expr} and if any
@@ -105,17 +122,93 @@ getLocs <- function(calls) {
#' @rdname stacktrace
#' @export
captureStackTraces <- function(expr) {
withCallingHandlers(expr,
error = function(e) {
if (is.null(attr(e, "stack.trace", exact = TRUE))) {
calls <- sys.calls()
attr(e, "stack.trace") <- calls
stop(e)
}
}
promises::with_promise_domain(createStackTracePromiseDomain(),
expr
)
}
#' @include globals.R
.globals$deepStack <- NULL
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)
# Subscription time
if (deepStacksEnabled()) {
currentStack <- sys.calls()
currentParents <- sys.parents()
attr(currentStack, "parents") <- currentParents
currentDeepStack <- .globals$deepStack
}
function(...) {
# Fulfill time
if (deepStacksEnabled()) {
origDeepStack <- .globals$deepStack
.globals$deepStack <- c(currentDeepStack, list(currentStack))
on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
}
withCallingHandlers(
onFulfilled(...),
error = doCaptureStack
)
}
},
wrapOnRejected = function(onRejected) {
force(onRejected)
# Subscription time
if (deepStacksEnabled()) {
currentStack <- sys.calls()
currentParents <- sys.parents()
attr(currentStack, "parents") <- currentParents
currentDeepStack <- .globals$deepStack
}
function(...) {
# Fulfill time
if (deepStacksEnabled()) {
origDeepStack <- .globals$deepStack
.globals$deepStack <- c(currentDeepStack, list(currentStack))
on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
}
withCallingHandlers(
onRejected(...),
error = doCaptureStack
)
}
},
wrapSync = function(expr) {
withCallingHandlers(expr,
error = doCaptureStack
)
},
onError = doCaptureStack
)
}
deepStacksEnabled <- function() {
getOption("shiny.deepstacktrace", TRUE)
}
doCaptureStack <- function(e) {
if (is.null(attr(e, "stack.trace", exact = TRUE))) {
calls <- sys.calls()
parents <- sys.parents()
attr(calls, "parents") <- parents
attr(e, "stack.trace") <- calls
}
if (deepStacksEnabled()) {
if (is.null(attr(e, "deep.stack.trace", exact = TRUE)) && !is.null(.globals$deepStack)) {
attr(e, "deep.stack.trace") <- .globals$deepStack
}
}
stop(e)
}
#' @details \code{withLogErrors} captures stack traces and logs errors that
#' occur in \code{expr}, but does allow errors to propagate beyond this point
#' (i.e. it doesn't catch the error). The same caveats that apply to
@@ -128,7 +221,22 @@ withLogErrors <- function(expr,
offset = getOption("shiny.stacktraceoffset", TRUE)) {
withCallingHandlers(
captureStackTraces(expr),
{
result <- captureStackTraces(expr)
# Handle expr being an async operation
if (promises::is.promise(result)) {
result <- promises::catch(result, function(cond) {
# Don't print shiny.silent.error (i.e. validation errors)
if (inherits(cond, "shiny.silent.error")) return()
if (isTRUE(getOption("show.error.messages"))) {
printError(cond, full = full, offset = offset)
}
})
}
result
},
error = function(cond) {
# Don't print shiny.silent.error (i.e. validation errors)
if (inherits(cond, "shiny.silent.error")) return()
@@ -158,11 +266,11 @@ withLogErrors <- function(expr,
printError <- function(cond,
full = getOption("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)
invisible()
}
#' @rdname stacktrace
@@ -171,24 +279,85 @@ printStackTrace <- function(cond,
full = getOption("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
stackTrace <- attr(cond, "stack.trace", exact = TRUE)
tryCatch(
if (!is.null(stackTrace)) {
message(paste0(
"Stack trace (innermost first):\n",
paste0(collapse = "\n",
formatStackTrace(stackTrace, full = full, offset = offset,
indent = " ")
)
))
} else {
message("No stack trace available")
},
error = function(cond) {
warning("Failed to write stack trace: ", cond)
}
should_drop <- !full
should_strip <- !full
should_prune <- !full
stackTraceCalls <- c(
attr(cond, "deep.stack.trace", exact = TRUE),
list(attr(cond, "stack.trace", exact = TRUE))
)
stackTraceParents <- lapply(stackTraceCalls, attr, which = "parents", exact = TRUE)
stackTraceCallNames <- lapply(stackTraceCalls, getCallNames)
stackTraceCalls <- lapply(stackTraceCalls, offsetSrcrefs, offset = offset)
# Use dropTrivialFrames logic to remove trailing bits (.handleSimpleError, h)
if (should_drop) {
# toKeep is a list of logical vectors, of which elements (stack frames) to keep
toKeep <- lapply(stackTraceCallNames, dropTrivialFrames)
# We apply the list of logical vector indices to each data structure
stackTraceCalls <- mapply(stackTraceCalls, FUN = `[`, toKeep, SIMPLIFY = FALSE)
stackTraceCallNames <- mapply(stackTraceCallNames, FUN = `[`, toKeep, SIMPLIFY = FALSE)
stackTraceParents <- mapply(stackTraceParents, FUN = `[`, toKeep, SIMPLIFY = FALSE)
}
delayedAssign("all_true", {
# List of logical vectors that are all TRUE, the same shape as
# stackTraceCallNames. Delay the evaluation so we don't create it unless
# we need it, but if we need it twice then we don't pay to create it twice.
lapply(stackTraceCallNames, function(st) {
rep_len(TRUE, length(st))
})
})
# stripStackTraces and lapply(stackTraceParents, pruneStackTrace) return lists
# of logical vectors. Use mapply(FUN = `&`) to boolean-and each pair of the
# logical vectors.
toShow <- mapply(
if (should_strip) stripStackTraces(stackTraceCallNames) else all_true,
if (should_prune) lapply(stackTraceParents, pruneStackTrace) else all_true,
FUN = `&`,
SIMPLIFY = FALSE
)
dfs <- mapply(seq_along(stackTraceCalls), rev(stackTraceCalls), rev(stackTraceCallNames), rev(toShow), FUN = function(i, calls, nms, index) {
st <- data.frame(
num = rev(which(index)),
call = rev(nms[index]),
loc = rev(getLocs(calls[index])),
category = rev(getCallCategories(calls[index])),
stringsAsFactors = FALSE
)
if (i != 1) {
message("From earlier call:")
}
if (nrow(st) == 0) {
message(" [No stack trace available]")
} else {
width <- floor(log10(max(st$num))) + 1
formatted <- paste0(
" ",
formatC(st$num, width = width),
": ",
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
if (category == "pkg")
crayon::silver(name)
else if (category == "user")
crayon::blue$bold(name)
else
crayon::white(name)
}),
"\n"
)
cat(file = stderr(), formatted, sep = "")
}
st
}, SIMPLIFY = FALSE)
invisible()
}
@@ -196,12 +365,17 @@ printStackTrace <- function(cond,
#' from \code{conditionStackTrace(cond)}) and returns a data frame with one
#' row for each stack frame and the columns \code{num} (stack frame number),
#' \code{call} (a function name or similar), and \code{loc} (source file path
#' and line number, if available).
#' and line number, if available). It was deprecated after shiny 1.0.5 because
#' it doesn't support deep stack traces.
#' @rdname stacktrace
#' @export
extractStackTrace <- function(calls,
full = getOption("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
shinyDeprecated(NULL,
"extractStackTrace is deprecated. Please contact the Shiny team if you were using this functionality.",
version = "1.0.5")
srcrefs <- getSrcRefs(calls)
if (offset) {
@@ -241,7 +415,11 @@ extractStackTrace <- function(calls,
score <- rep.int(0, length(callnames))
score[callnames == "..stacktraceoff.."] <- -1
score[callnames == "..stacktraceon.."] <- 1
toShow <- (1 + cumsum(score)) > 0 & !(callnames %in% c("..stacktraceon..", "..stacktraceoff.."))
toShow <- (1 + cumsum(score)) > 0 & !(callnames %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
# doTryCatch, tryCatchOne, and tryCatchList are not informative--they're
# just internals for tryCatch
toShow <- toShow & !(callnames %in% c("doTryCatch", "tryCatchOne", "tryCatchList"))
}
calls <- calls[toShow]
@@ -253,12 +431,115 @@ extractStackTrace <- function(calls,
num = index,
call = getCallNames(calls),
loc = getLocs(calls),
category = getCallCategories(calls),
stringsAsFactors = FALSE
)
}
stripStackTraces <- function(stackTraces, values = FALSE) {
score <- 1L # >=1: show, <=0: hide
lapply(seq_along(stackTraces), function(i) {
res <- stripOneStackTrace(stackTraces[[i]], i != 1, score)
score <<- res$score
toShow <- as.logical(res$trace)
if (values) {
as.character(stackTraces[[i]][toShow])
} else {
as.logical(toShow)
}
})
}
stripOneStackTrace <- function(stackTrace, truncateFloor, startingScore) {
prefix <- logical(0)
if (truncateFloor) {
indexOfFloor <- utils::tail(which(stackTrace == "..stacktracefloor.."), 1)
if (length(indexOfFloor)) {
stackTrace <- stackTrace[(indexOfFloor+1L):length(stackTrace)]
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))
}
# Given sys.parents() (which corresponds to sys.calls()), return a logical index
# that prunes each subtree so that only the final branch remains. The result,
# when applied to sys.calls(), is a linear list of calls without any "wrapper"
# functions like tryCatch, try, with, hybrid_chain, etc. While these are often
# part of the active call stack, they rarely are helpful when trying to identify
# a broken bit of code.
pruneStackTrace <- function(parents) {
# Detect nodes that are not the last child. This is necessary, but not
# 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.
include <- vapply(seq_along(parents), function(i) {
if (!is_dupe[[i]] && parents[[i]] == current_node) {
current_node <<- i
TRUE
} else {
FALSE
}
}, FUN.VALUE = logical(1))
include
}
dropTrivialFrames <- function(callnames) {
# Remove stop(), .handleSimpleError(), and h() calls from the end of
# the calls--they don't add any helpful information. But only remove
# the last *contiguous* block of them, and then, only if they are the
# last thing in the calls list.
hideable <- callnames %in% c(".handleSimpleError", "h", "base$wrapOnFulfilled")
# 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)
)
}
offsetSrcrefs <- function(calls, offset = TRUE) {
if (offset) {
srcrefs <- getSrcRefs(calls)
# Offset calls vs. srcrefs by 1 to make them more intuitive.
# E.g. for "foo [bar.R:10]", line 10 of bar.R will be part of
# the definition of foo().
srcrefs <- c(utils::tail(srcrefs, -1), list(NULL))
calls <- setSrcRefs(calls, srcrefs)
}
calls
}
#' @details \code{formatStackTrace} is similar to \code{extractStackTrace}, but
#' it returns a preformatted character vector instead of a data frame.
#' it returns a preformatted character vector instead of a data frame. It was
#' deprecated after shiny 1.0.5 because it doesn't support deep stack traces.
#' @param indent A string to prefix every line of the stack trace.
#' @rdname stacktrace
#' @export
@@ -266,6 +547,10 @@ formatStackTrace <- function(calls, indent = " ",
full = getOption("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
shinyDeprecated(NULL,
"extractStackTrace is deprecated. Please contact the Shiny team if you were using this functionality.",
version = "1.0.5")
st <- extractStackTrace(calls, full = full, offset = offset)
if (nrow(st) == 0) {
return(character(0))
@@ -276,8 +561,14 @@ formatStackTrace <- function(calls, indent = " ",
indent,
formatC(st$num, width = width),
": ",
st$call,
st$loc
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
if (category == "pkg")
crayon::silver(name)
else if (category == "user")
crayon::blue$bold(name)
else
crayon::white(name)
})
)
}
@@ -332,3 +623,5 @@ conditionStackTrace <- function(cond) {
#' @rdname stacktrace
#' @export
..stacktraceoff.. <- function(expr) expr
..stacktracefloor.. <- function(expr) expr

View File

@@ -20,6 +20,18 @@
# form upload, i.e. traditional HTTP POST-based file upload) doesn't work with
# the websockets package's HTTP server at the moment.
# @description Returns a file's extension, with a leading dot, if one can be
# found. A valid extension contains only alphanumeric characters. If there is
# no extension, or if it contains non-alphanumeric characters, an empty
# string is returned.
# @param x character vector giving file paths.
# @return The extension of \code{x}, with a leading dot, if one was found.
# Otherwise, an empty character vector.
maybeGetExtension <- function(x) {
ext <- tools::file_ext(x)
ifelse(ext == "", ext, paste0(".", ext))
}
FileUploadOperation <- R6Class(
'FileUploadOperation',
portable = FALSE,
@@ -52,8 +64,9 @@ FileUploadOperation <- R6Class(
.currentFileInfo <<- file
.pendingFileInfos <<- tail(.pendingFileInfos, -1)
filename <- file.path(.dir, as.character(length(.files$name)))
row <- data.frame(name=file$name, size=file$size, type=file$type,
fileBasename <- basename(.currentFileInfo$name)
filename <- file.path(.dir, paste0(as.character(length(.files$name)), maybeGetExtension(fileBasename)))
row <- data.frame(name=fileBasename, size=file$size, type=file$type,
datapath=filename, stringsAsFactors=FALSE)
if (length(.files$name) == 0)
@@ -94,7 +107,7 @@ FileUploadContext <- R6Class(
},
createUploadOperation = function(fileInfos) {
while (TRUE) {
id <- paste(as.raw(p_runif(12, min=0, max=0xFF)), collapse='')
id <- createUniqueId(12)
private$ids <- c(private$ids, id)
dir <- file.path(private$basedir, id)
if (!dir.create(dir))

View File

@@ -5,7 +5,7 @@
# R's lazy-loading package scheme causes the private seed to be cached in the
# package itself, making our PRNG completely deterministic. This line resets
# the private seed during load.
withPrivateSeed(reinitializeSeed())
withPrivateSeed(set.seed(NULL))
}
.onAttach <- function(libname, pkgname) {

View File

@@ -97,8 +97,8 @@ renderReactLog <- function(sessionToken = NULL, time = TRUE) {
.graphAppend(list(action='enter', id=id))
}
.graphExitContext <- function(id) {
.graphAppend(list(action='exit', id=id))
.graphExitContext <- function(id, domain) {
.graphAppend(list(action='exit', id=id), domain = domain)
}
.graphValueChange <- function(label, value) {

95
R/history.R Normal file
View File

@@ -0,0 +1,95 @@
#' @include reactive-domains.R
NULL
#' @include reactives.R
NULL
#' Get the query string / hash component from the URL
#'
#' Two user friendly wrappers for getting the query string and the hash
#' component from the app's URL.
#'
#' These can be particularly useful if you want to display different content
#' depending on the values in the query string / hash (e.g. instead of basing
#' the conditional on an input or a calculated reactive, you can base it on the
#' query string). However, note that, if you're changing the query string / hash
#' programatically from within the server code, you must use
#' \code{updateQueryString(_yourNewQueryString_, mode = "push")}. The default
#' \code{mode} for \code{updateQueryString} is \code{"replace"}, which doesn't
#' raise any events, so any observers or reactives that depend on it will
#' \emph{not} get triggered. However, if you're changing the query string / hash
#' directly by typing directly in the browser and hitting enter, you don't have
#' to worry about this.
#'
#' @param session A Shiny session object.
#'
#' @return For \code{getQueryString}, a named list. For example, the query
#' string \code{?param1=value1&param2=value2} becomes \code{list(param1 =
#' value1, param2 = value2)}. For \code{getUrlHash}, a character vector with
#' the hash (including the leading \code{#} symbol).
#'
#' @seealso \code{\link{updateQueryString}}
#'
#' @examples
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#'
#' ## App 1: getQueryString
#' ## Printing the value of the query string
#' ## (Use the back and forward buttons to see how the browser
#' ## keeps a record of each state)
#' shinyApp(
#' ui = fluidPage(
#' textInput("txt", "Enter new query string"),
#' helpText("Format: ?param1=val1&param2=val2"),
#' actionButton("go", "Update"),
#' hr(),
#' verbatimTextOutput("query")
#' ),
#' server = function(input, output, session) {
#' observeEvent(input$go, {
#' updateQueryString(input$txt, mode = "push")
#' })
#' output$query <- renderText({
#' query <- getQueryString()
#' queryText <- paste(names(query), query,
#' sep = "=", collapse=", ")
#' paste("Your query string is:\n", queryText)
#' })
#' }
#' )
#'
#' ## App 2: getUrlHash
#' ## Printing the value of the URL hash
#' ## (Use the back and forward buttons to see how the browser
#' ## keeps a record of each state)
#' shinyApp(
#' ui = fluidPage(
#' textInput("txt", "Enter new hash"),
#' helpText("Format: #hash"),
#' actionButton("go", "Update"),
#' hr(),
#' verbatimTextOutput("hash")
#' ),
#' server = function(input, output, session) {
#' observeEvent(input$go, {
#' updateQueryString(input$txt, mode = "push")
#' })
#' output$hash <- renderText({
#' hash <- getUrlHash()
#' paste("Your hash is:\n", hash)
#' })
#' }
#' )
#' }
#' @export
getQueryString <- function(session = getDefaultReactiveDomain()) {
parseQueryString(session$clientData$url_search)
}
#' @rdname getQueryString
#' @export
getUrlHash <- function(session = getDefaultReactiveDomain()) {
session$clientData$url_hash
}

View File

@@ -6,13 +6,18 @@
#' URL.
#'
#' @param dependency A single HTML dependency object, created using
#' \code{\link[htmltools]{htmlDependency}}. If the \code{src} value is named, then
#' \code{href} and/or \code{file} names must be present.
#' \code{\link[htmltools]{htmlDependency}}. If the \code{src} value is named,
#' then \code{href} and/or \code{file} names must be present.
#' @param scrubFile If TRUE (the default), remove \code{src$file} for the
#' dependency. This prevents the local file path from being sent to the client
#' when dynamic web dependencies are used. If FALSE, don't remove
#' \code{src$file}. Setting it to FALSE should be needed only in very unusual
#' cases.
#'
#' @return A single HTML dependency object that has an \code{href}-named element
#' in its \code{src}.
#' @export
createWebDependency <- function(dependency) {
createWebDependency <- function(dependency, scrubFile = TRUE) {
if (is.null(dependency))
return(NULL)
@@ -25,6 +30,10 @@ createWebDependency <- function(dependency) {
dependency$src$href <- prefix
}
# Don't leak local file path to client
if (scrubFile)
dependency$src$file <- NULL
return(dependency)
}

View File

@@ -86,6 +86,8 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
if (use_x) {
if (is.null(xvar))
stop("brushedPoints: not able to automatically infer `xvar` from brush")
if (!(xvar %in% names(df)))
stop("brushedPoints: `xvar` ('", xvar ,"') not in names of input")
# Extract data values from the data frame
x <- asNumber(df[[xvar]])
keep_rows <- keep_rows & (x >= brush$xmin & x <= brush$xmax)
@@ -93,6 +95,8 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
if (use_y) {
if (is.null(yvar))
stop("brushedPoints: not able to automatically infer `yvar` from brush")
if (!(yvar %in% names(df)))
stop("brushedPoints: `yvar` ('", yvar ,"') not in names of input")
y <- asNumber(df[[yvar]])
keep_rows <- keep_rows & (y >= brush$ymin & y <= brush$ymax)
}
@@ -118,6 +122,19 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
# $ xmax : num 4.22
# $ ymin : num 13.9
# $ ymax : num 19.8
# $ coords_css:List of 4
# ..$ xmin: int 260
# ..$ xmax: int 298
# ..$ ymin: num 112
# ..$ ymax: num 205
# $ coords_img:List of 4
# ..$ xmin: int 325
# ..$ xmax: num 372
# ..$ ymin: num 140
# ..$ ymax: num 257
# $ img_css_ratio:List of 2
# ..$ x: num 1.25
# ..$ y: num 1.25
# $ mapping: Named list()
# $ domain :List of 4
# ..$ left : num 1.36
@@ -143,6 +160,19 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
# $ ymax : num 20.4
# $ panelvar1: int 6
# $ panelvar2: int 0
# $ coords_css:List of 4
# ..$ xmin: int 260
# ..$ xmax: int 298
# ..$ ymin: num 112
# ..$ ymax: num 205
# $ coords_img:List of 4
# ..$ xmin: int 325
# ..$ xmax: num 372
# ..$ ymin: num 140
# ..$ ymax: num 257
# $ img_css_ratio:List of 2
# ..$ x: num 1.25
# ..$ y: num 1.25
# $ mapping :List of 4
# ..$ x : chr "wt"
# ..$ y : chr "mpg"
@@ -245,18 +275,29 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
if (is.null(yvar))
stop("nearPoints: not able to automatically infer `yvar` from coordinfo")
if (!(xvar %in% names(df)))
stop("nearPoints: `xvar` ('", xvar ,"') not in names of input")
if (!(yvar %in% names(df)))
stop("nearPoints: `yvar` ('", yvar ,"') not in names of input")
# Extract data values from the data frame
x <- asNumber(df[[xvar]])
y <- asNumber(df[[yvar]])
# Get the pixel coordinates of the point
coordPx <- scaleCoords(coordinfo$x, coordinfo$y, coordinfo)
# Get the coordinates of the point (in img pixel coordinates)
point_img <- coordinfo$coords_img
# Get pixel coordinates of data points
dataPx <- scaleCoords(x, y, coordinfo)
# Get coordinates of data points (in img pixel coordinates)
data_img <- scaleCoords(x, y, coordinfo)
# Distances of data points to coordPx
dists <- sqrt((dataPx$x - coordPx$x) ^ 2 + (dataPx$y - coordPx$y) ^ 2)
# Get x/y distances (in css coordinates)
dist_css <- list(
x = (data_img$x - point_img$x) / coordinfo$img_css_ratio$x,
y = (data_img$y - point_img$y) / coordinfo$img_css_ratio$y
)
# Distances of data points to the target point, in css pixels.
dists <- sqrt(dist_css$x^2 + dist_css$y^2)
if (addDist)
df$dist_ <- dists
@@ -298,50 +339,68 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
# The coordinfo data structure will look something like the examples below.
# For base graphics, `mapping` is empty, and there are no panelvars:
# List of 7
# $ x : num 4.37
# $ y : num 12
# $ mapping: Named list()
# $ domain :List of 4
# $ x : num 4.37
# $ y : num 12
# $ coords_css:List of 2
# ..$ x: int 286
# ..$ y: int 192
# $ coords_img:List of 2
# ..$ x: num 358
# ..$ y: int 240
# $ img_css_ratio:List of 2
# ..$ x: num 1.25
# ..$ y: num 1.25
# $ mapping : Named list()
# $ domain :List of 4
# ..$ left : num 1.36
# ..$ right : num 5.58
# ..$ bottom: num 9.46
# ..$ top : num 34.8
# $ range :List of 4
# $ range :List of 4
# ..$ left : num 58
# ..$ right : num 429
# ..$ bottom: num 226
# ..$ top : num 58
# $ log :List of 2
# $ log :List of 2
# ..$ x: NULL
# ..$ y: NULL
# $ .nonce : num 0.343
# $ .nonce : num 0.343
#
# For ggplot2, the mapping vars usually will be included, and if faceting is
# used, they will be listed as panelvars:
# List of 9
# $ x : num 3.78
# $ y : num 17.1
# $ panelvar1: int 6
# $ panelvar2: int 0
# $ mapping :List of 4
# $ x : num 3.78
# $ y : num 17.1
# $ coords_css:List of 2
# ..$ x: int 286
# ..$ y: int 192
# $ coords_img:List of 2
# ..$ x: num 358
# ..$ y: int 240
# $ img_css_ratio:List of 2
# ..$ x: num 1.25
# ..$ y: num 1.25
# $ panelvar1 : int 6
# $ panelvar2 : int 0
# $ mapping :List of 4
# ..$ x : chr "wt"
# ..$ y : chr "mpg"
# ..$ panelvar1: chr "cyl"
# ..$ panelvar2: chr "am"
# $ domain :List of 4
# $ domain :List of 4
# ..$ left : num 1.32
# ..$ right : num 5.62
# ..$ bottom: num 9.22
# ..$ top : num 35.1
# $ range :List of 4
# $ range :List of 4
# ..$ left : num 172
# ..$ right : num 300
# ..$ bottom: num 144
# ..$ top : num 28.5
# $ log :List of 2
# $ log :List of 2
# ..$ x: NULL
# ..$ y: NULL
# $ .nonce : num 0.603
# $ .nonce : num 0.603

View File

@@ -1,3 +1,33 @@
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")) {
pngfun <- grDevices::png
} else if ((getOption('shiny.usecairo') %OR% TRUE) &&
nchar(system.file(package = "Cairo"))) {
pngfun <- Cairo::CairoPNG
} else {
pngfun <- grDevices::png
}
pngfun(filename=filename, width=width, height=height, res=res, ...)
# 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.
# 200x50), we will get an error "figure margin too large", which is triggered
# by plot.new() with the default (large) margin. However, this does not
# guarantee user's code in func() will not trigger the error -- they may have
# to set par(mar = smaller_value) before they draw base graphics.
op <- graphics::par(mar = rep(0, 4))
tryCatch(
graphics::plot.new(),
finally = graphics::par(op)
)
grDevices::dev.cur()
}
#' Run a plotting function and save the output as a PNG
#'
#' This function returns the name of the PNG file that it generates. In
@@ -21,42 +51,51 @@
#' @param width Width in pixels.
#' @param height Height in pixels.
#' @param res Resolution in pixels per inch. This value is passed to
#' \code{\link{png}}. Note that this affects the resolution of PNG rendering in
#' \code{\link[grDevices]{png}}. Note that this affects the resolution of PNG rendering in
#' R; it won't change the actual ppi of the browser.
#' @param ... Arguments to be passed through to \code{\link[grDevices]{png}}.
#' These can be used to set the width, height, background color, etc.
#' @export
plotPNG <- function(func, filename=tempfile(fileext='.png'),
width=400, height=400, res=72, ...) {
# 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")) {
pngfun <- grDevices::png
} else if ((getOption('shiny.usecairo') %OR% TRUE) &&
nchar(system.file(package = "Cairo"))) {
pngfun <- Cairo::CairoPNG
} else {
pngfun <- grDevices::png
}
pngfun(filename=filename, width=width, height=height, res=res, ...)
# 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.
# 200x50), we will get an error "figure margin too large", which is triggered
# by plot.new() with the default (large) margin. However, this does not
# guarantee user's code in func() will not trigger the error -- they may have
# to set par(mar = smaller_value) before they draw base graphics.
op <- graphics::par(mar = rep(0, 4))
tryCatch(
graphics::plot.new(),
finally = graphics::par(op)
)
dv <- grDevices::dev.cur()
dv <- startPNG(filename, width, height, res, ...)
on.exit(grDevices::dev.off(dv), add = TRUE)
func()
filename
}
#' @importFrom grDevices dev.set dev.cur
createGraphicsDevicePromiseDomain <- function(which = dev.cur()) {
force(which)
promises::new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
function(...) {
old <- dev.cur()
dev.set(which)
on.exit(dev.set(old))
onFulfilled(...)
}
},
wrapOnRejected = function(onRejected) {
force(onRejected)
function(...) {
old <- dev.cur()
dev.set(which)
on.exit(dev.set(old))
onRejected(...)
}
},
wrapSync = function(expr) {
old <- dev.cur()
dev.set(which)
on.exit(dev.set(old))
force(expr)
}
)
}

View File

@@ -6,9 +6,22 @@
#'
#' @inheritParams textInput
#' @param choices List of values to show checkboxes for. If elements of the list
#' are named then that name rather than the value is displayed to the user.
#' are named then that name rather than the value is displayed to the user. If
#' this argument is provided, then \code{choiceNames} and \code{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 values that should be initially selected, if any.
#' @param inline If \code{TRUE}, render the choices inline (i.e. horizontally)
#' @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, \code{choiceNames} and \code{choiceValues}
#' must have the same length). If either of these arguments is
#' provided, then the other \emph{must} be provided and \code{choices}
#' \emph{must not} be provided. The advantage of using both of these over
#' a named list for \code{choices} is that \code{choiceNames} allows any
#' type of UI object to be passed through (tag objects, icons, HTML code,
#' ...), instead of just simple text. See Examples.
#'
#' @return A list of HTML elements that can be added to a UI definition.
#'
#' @family input elements
@@ -26,26 +39,52 @@
#' tableOutput("data")
#' )
#'
#' server <- function(input, output) {
#' server <- function(input, output, session) {
#' output$data <- renderTable({
#' mtcars[, c("mpg", input$variable), drop = FALSE]
#' }, rownames = TRUE)
#' }
#'
#' shinyApp(ui, server)
#'
#' ui <- fluidPage(
#' checkboxGroupInput("icons", "Choose icons:",
#' choiceNames =
#' list(icon("calendar"), icon("bed"),
#' icon("cog"), icon("bug")),
#' choiceValues =
#' list("calendar", "bed", "cog", "bug")
#' ),
#' textOutput("txt")
#' )
#'
#' server <- function(input, output, session) {
#' output$txt <- renderText({
#' icons <- paste(input$icons, collapse = ", ")
#' paste("You chose", icons)
#' })
#' }
#'
#' shinyApp(ui, server)
#' }
#' @export
checkboxGroupInput <- function(inputId, label, choices, selected = NULL,
inline = FALSE, width = NULL) {
checkboxGroupInput <- function(inputId, label, choices = NULL, selected = NULL,
inline = FALSE, width = NULL, choiceNames = NULL, choiceValues = NULL) {
# keep backward compatibility with Shiny < 1.0.1 (see #1649)
if (is.null(choices) && is.null(choiceNames) && is.null(choiceValues)) {
choices <- character(0)
}
args <- normalizeChoicesArgs(choices, choiceNames, choiceValues)
selected <- restoreInput(id = inputId, default = selected)
# resolve names
choices <- choicesWithNames(choices)
if (!is.null(selected))
selected <- validateSelected(selected, choices, inputId)
# default value if it's not specified
if (!is.null(selected)) selected <- as.character(selected)
options <- generateOptions(inputId, choices, selected, inline)
options <- generateOptions(inputId, selected, inline,
'checkbox', args$choiceNames, args$choiceValues)
divClass <- "form-group shiny-input-checkboxgroup shiny-input-container"
if (inline)

View File

@@ -10,7 +10,7 @@
#' \item \code{yy} Year without century (12)
#' \item \code{yyyy} Year with century (2012)
#' \item \code{mm} Month number, with leading zero (01-12)
#' \item \code{m} Month number, without leading zero (01-12)
#' \item \code{m} Month number, without leading zero (1-12)
#' \item \code{M} Abbreviated month name
#' \item \code{MM} Full month name
#' \item \code{dd} Day of month with leading zero
@@ -21,23 +21,33 @@
#'
#' @inheritParams textInput
#' @param value The starting date. Either a Date object, or a string in
#' \code{yyyy-mm-dd} format. If NULL (the default), will use the current
#' date in the client's time zone.
#' \code{yyyy-mm-dd} format. If NULL (the default), will use the current date
#' in the client's time zone.
#' @param min The minimum allowed date. Either a Date object, or a string in
#' \code{yyyy-mm-dd} format.
#' @param max The maximum allowed date. Either a Date object, or a string in
#' \code{yyyy-mm-dd} format.
#' @param format The format of the date to display in the browser. Defaults to
#' \code{"yyyy-mm-dd"}.
#' @param startview The date range shown when the input object is first
#' clicked. Can be "month" (the default), "year", or "decade".
#' @param startview The date range shown when the input object is first clicked.
#' Can be "month" (the default), "year", or "decade".
#' @param weekstart Which day is the start of the week. Should be an integer
#' from 0 (Sunday) to 6 (Saturday).
#' @param language The language used for month and day names. Default is "en".
#' Other valid values include "bg", "ca", "cs", "da", "de", "el", "es", "fi",
#' "fr", "he", "hr", "hu", "id", "is", "it", "ja", "kr", "lt", "lv", "ms",
#' "nb", "nl", "pl", "pt", "pt-BR", "ro", "rs", "rs-latin", "ru", "sk", "sl",
#' "sv", "sw", "th", "tr", "uk", "zh-CN", and "zh-TW".
#' Other valid values include "ar-tn", "ar", "az", "bg", "bn", "br", "bs",
#' "ca", "cs", "cy", "da", "de", "el", "en-AU", "en-CA", "en-GB", "en-IE",
#' "en-NZ", "en-ZA", "eo", "es", "et", "eu", "fa", "fi", "fo", "fr-CH", "fr",
#' "gl", "he", "hi", "hr", "hu", "hy", "id", "is", "it-CH", "it", "ja", "ka",
#' "kh", "kk", "km", "ko", "kr", "lt", "lv", "me", "mk", "mn", "ms", "nl-BE",
#' "nl", "no", "oc", "pl", "pt-BR", "pt", "ro", "rs-latin", "rs", "ru", "si",
#' "sk", "sl", "sq", "sr-latin", "sr", "sv", "sw", "ta", "tg", "th", "tk",
#' "tr", "uk", "uz-cyrl", "uz-latn", "vi", "zh-CN", and "zh-TW".
#' @param autoclose Whether or not to close the datepicker immediately when a
#' date is selected.
#' @param datesdisabled Which dates should be disabled. Either a Date object, or
#' a string in \code{yyyy-mm-dd} format.
#' @param daysofweekdisabled Days of the week that should be disabled. Should be
#' a integer vector with values from 0 (Sunday) to 6 (Saturday).
#'
#' @family input elements
#' @seealso \code{\link{dateRangeInput}}, \code{\link{updateDateInput}}
@@ -60,52 +70,76 @@
#'
#' # Use different language and different first day of week
#' dateInput("date5", "Date:",
#' language = "de",
#' language = "ru",
#' weekstart = 1),
#'
#' # Start with decade view instead of default month view
#' dateInput("date6", "Date:",
#' startview = "decade")
#' startview = "decade"),
#'
#' # Disable Mondays and Tuesdays.
#' dateInput("date7", "Date:", daysofweekdisabled = c(1,2)),
#'
#' # Disable specific dates.
#' dateInput("date8", "Date:", value = "2012-02-29",
#' datesdisabled = c("2012-03-01", "2012-03-02"))
#' )
#'
#' shinyApp(ui, server = function(input, output) { })
#' }
#' @export
dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
format = "yyyy-mm-dd", startview = "month", weekstart = 0, language = "en",
width = NULL) {
format = "yyyy-mm-dd", startview = "month", weekstart = 0,
language = "en", width = NULL, autoclose = TRUE,
datesdisabled = NULL, daysofweekdisabled = NULL) {
# If value is a date object, convert it to a string with yyyy-mm-dd format
# Same for min and max
if (inherits(value, "Date")) value <- format(value, "%Y-%m-%d")
if (inherits(min, "Date")) min <- format(min, "%Y-%m-%d")
if (inherits(max, "Date")) max <- format(max, "%Y-%m-%d")
if (inherits(datesdisabled, "Date")) {
datesdisabled <- format(datesdisabled, "%Y-%m-%d")
}
value <- restoreInput(id = inputId, default = value)
attachDependencies(
tags$div(id = inputId,
class = "shiny-date-input form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
tags$div(id = inputId,
class = "shiny-date-input form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
controlLabel(inputId, label),
tags$input(type = "text",
# datepicker class necessary for dropdown to display correctly
class = "form-control datepicker",
`data-date-language` = language,
`data-date-weekstart` = weekstart,
`data-date-format` = format,
`data-date-start-view` = startview,
`data-min-date` = min,
`data-max-date` = max,
`data-initial-date` = value
)
controlLabel(inputId, label),
tags$input(type = "text",
class = "form-control",
`data-date-language` = language,
`data-date-week-start` = weekstart,
`data-date-format` = format,
`data-date-start-view` = startview,
`data-min-date` = min,
`data-max-date` = max,
`data-initial-date` = value,
`data-date-autoclose` = if (autoclose) "true" else "false",
`data-date-dates-disabled` =
# Ensure NULL is not sent as `{}` but as 'null'
jsonlite::toJSON(datesdisabled, null = 'null'),
`data-date-days-of-week-disabled` =
jsonlite::toJSON(daysofweekdisabled, null = 'null')
),
datePickerDependency
)
}
datePickerDependency <- htmlDependency(
"bootstrap-datepicker", "1.0.2", c(href = "shared/datepicker"),
"bootstrap-datepicker",
"1.8.0",
c(href = "shared/datepicker"),
script = "js/bootstrap-datepicker.min.js",
stylesheet = "css/datepicker.css")
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>"
)

View File

@@ -10,7 +10,7 @@
#' \item \code{yy} Year without century (12)
#' \item \code{yyyy} Year with century (2012)
#' \item \code{mm} Month number, with leading zero (01-12)
#' \item \code{m} Month number, without leading zero (01-12)
#' \item \code{m} Month number, without leading zero (1-12)
#' \item \code{M} Abbreviated month name
#' \item \code{MM} Full month name
#' \item \code{dd} Day of month with leading zero
@@ -73,7 +73,8 @@
#' @export
dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
min = NULL, max = NULL, format = "yyyy-mm-dd", startview = "month",
weekstart = 0, language = "en", separator = " to ", width = NULL) {
weekstart = 0, language = "en", separator = " to ", width = NULL,
autoclose = TRUE) {
# If start and end are date objects, convert to a string with yyyy-mm-dd format
# Same for min and max
@@ -98,24 +99,26 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
class = "input-sm form-control",
type = "text",
`data-date-language` = language,
`data-date-weekstart` = weekstart,
`data-date-week-start` = weekstart,
`data-date-format` = format,
`data-date-start-view` = startview,
`data-min-date` = min,
`data-max-date` = max,
`data-initial-date` = start
`data-initial-date` = start,
`data-date-autoclose` = if (autoclose) "true" else "false"
),
span(class = "input-group-addon", separator),
tags$input(
class = "input-sm form-control",
type = "text",
`data-date-language` = language,
`data-date-weekstart` = weekstart,
`data-date-week-start` = weekstart,
`data-date-format` = format,
`data-date-start-view` = startview,
`data-min-date` = min,
`data-max-date` = max,
`data-initial-date` = end
`data-initial-date` = end,
`data-date-autoclose` = if (autoclose) "true" else "false"
)
)
),

View File

@@ -27,6 +27,9 @@
#' Internet Explorer 9 and earlier.}
#' @param accept A character vector of MIME types; gives the browser a hint of
#' what kind of files the server is expecting.
#' @param buttonLabel The label used on the button. Can be text or an HTML tag
#' object.
#' @param placeholder The text to show before a file has been uploaded.
#'
#' @examples
#' ## Only run examples in interactive R sessions
@@ -70,7 +73,7 @@
#' }
#' @export
fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
width = NULL) {
width = NULL, buttonLabel = "Browse...", placeholder = "No file selected") {
restoredValue <- restoreInput(id = inputId, default = NULL)
@@ -105,12 +108,12 @@ fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
div(class = "input-group",
tags$label(class = "input-group-btn",
span(class = "btn btn-default btn-file",
"Browse...",
buttonLabel,
inputTag
)
),
tags$input(type = "text", class = "form-control",
placeholder = "No file selected", readonly = "readonly"
placeholder = placeholder, readonly = "readonly"
)
),

View File

@@ -3,19 +3,30 @@
#' Create a set of radio buttons used to select an item from a list.
#'
#' If you need to represent a "None selected" state, it's possible to default
#' the radio buttons to have no options selected by using
#' \code{selected = character(0)}. However, this is not recommended, as it gives
#' the user no way to return to that state once they've made a selection.
#' Instead, consider having the first of your choices be \code{c("None selected"
#' = "")}.
#' the radio buttons to have no options selected by using \code{selected =
#' character(0)}. However, this is not recommended, as it gives the user no way
#' to return to that state once they've made a selection. Instead, consider
#' having the first of your choices be \code{c("None selected" = "")}.
#'
#' @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)
#' @param selected The initially selected value (if not specified then
#' defaults to the first value)
#' named then that name rather than the value is displayed to the user). If
#' this argument is provided, then \code{choiceNames} and \code{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)
#' @param inline If \code{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, \code{choiceNames} and \code{choiceValues} must have the same
#' length). If either of these arguments is provided, then the other
#' \emph{must} be provided and \code{choices} \emph{must not} be provided. The
#' advantage of using both of these over a named list for \code{choices} is
#' that \code{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 \code{\link{updateRadioButtons}}
@@ -47,27 +58,46 @@
#' }
#'
#' shinyApp(ui, server)
#'
#' ui <- fluidPage(
#' radioButtons("rb", "Choose one:",
#' choiceNames = list(
#' icon("calendar"),
#' HTML("<p style='color:red;'>Red Text</p>"),
#' "Normal text"
#' ),
#' choiceValues = list(
#' "icon", "html", "text"
#' )),
#' textOutput("txt")
#' )
#'
#' server <- function(input, output) {
#' output$txt <- renderText({
#' paste("You chose", input$rb)
#' })
#' }
#'
#' shinyApp(ui, server)
#' }
#' @export
radioButtons <- function(inputId, label, choices, selected = NULL,
inline = FALSE, width = NULL) {
radioButtons <- function(inputId, label, choices = NULL, selected = NULL,
inline = FALSE, width = NULL, choiceNames = NULL, choiceValues = NULL) {
# resolve names
choices <- choicesWithNames(choices)
args <- normalizeChoicesArgs(choices, choiceNames, choiceValues)
selected <- restoreInput(id = inputId, default = selected)
# default value if it's not specified
selected <- if (is.null(selected)) choices[[1]] else {
validateSelected(selected, choices, inputId)
}
selected <- if (is.null(selected)) args$choiceValues[[1]] else as.character(selected)
if (length(selected) > 1) stop("The 'selected' argument must be of length 1")
options <- generateOptions(inputId, choices, selected, inline, type = 'radio')
options <- generateOptions(inputId, selected, inline,
'radio', args$choiceNames, args$choiceValues)
divClass <- "form-group shiny-input-radiogroup shiny-input-container"
if (inline)
divClass <- paste(divClass, "shiny-input-container-inline")
if (inline) divClass <- paste(divClass, "shiny-input-container-inline")
tags$div(id = inputId,
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),

View File

@@ -5,7 +5,7 @@
#'
#' By default, \code{selectInput()} and \code{selectizeInput()} use the
#' JavaScript library \pkg{selectize.js}
#' (\url{https://github.com/brianreavis/selectize.js}) to instead of the basic
#' (\url{https://github.com/selectize/selectize.js}) to instead of the basic
#' select input element. To use the standard HTML select input element, use
#' \code{selectInput()} with \code{selectize=FALSE}.
#'
@@ -15,7 +15,12 @@
#'
#' @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.
#' named, then that name rather than the value is displayed to the user.
#' This can also be a named list whose elements are (either named or
#' unnamed) lists or vectors. If this is the case, the outermost names
#' will be used as the "optgroup" label for the elements in the respective
#' sublist. This allows you to group and label similar choices. See the
#' example section for a small demo of this feature.
#' @param selected The initially selected value (or multiple values if
#' \code{multiple = TRUE}). If not specified then defaults to the first value
#' for single-select lists and no values for multiple select lists.
@@ -28,32 +33,49 @@
#' @return A select list control that can be added to a UI definition.
#'
#' @family input elements
#' @seealso \code{\link{updateSelectInput}}
#' @seealso \code{\link{updateSelectInput}} \code{\link{varSelectInput}}
#'
#' @examples
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
#'
#' ui <- fluidPage(
#' selectInput("variable", "Variable:",
#' c("Cylinders" = "cyl",
#' "Transmission" = "am",
#' "Gears" = "gear")),
#' tableOutput("data")
#' # basic example
#' shinyApp(
#' ui = fluidPage(
#' selectInput("variable", "Variable:",
#' c("Cylinders" = "cyl",
#' "Transmission" = "am",
#' "Gears" = "gear")),
#' tableOutput("data")
#' ),
#' server = function(input, output) {
#' output$data <- renderTable({
#' mtcars[, c("mpg", input$variable), drop = FALSE]
#' }, rownames = TRUE)
#' }
#' )
#'
#' server <- function(input, output) {
#' output$data <- renderTable({
#' mtcars[, c("mpg", input$variable), drop = FALSE]
#' }, rownames = TRUE)
#' }
#'
#' shinyApp(ui, server)
#' # demoing optgroup support in the `choices` arg
#' shinyApp(
#' ui = fluidPage(
#' selectInput("state", "Choose a state:",
#' list(`East Coast` = list("NY", "NJ", "CT"),
#' `West Coast` = list("WA", "OR", "CA"),
#' `Midwest` = list("MN", "WI", "IA"))
#' ),
#' textOutput("result")
#' ),
#' server = function(input, output) {
#' output$result <- renderText({
#' paste("You chose", input$state)
#' })
#' }
#' )
#' }
#' @export
selectInput <- function(inputId, label, choices, selected = NULL,
multiple = FALSE, selectize = TRUE, width = NULL,
size = NULL) {
multiple = FALSE, selectize = TRUE, width = NULL,
size = NULL) {
selected <- restoreInput(id = inputId, default = selected)
@@ -63,7 +85,7 @@ selectInput <- function(inputId, label, choices, selected = NULL,
# default value if it's not specified
if (is.null(selected)) {
if (!multiple) selected <- firstChoice(choices)
} else selected <- validateSelected(selected, choices, inputId)
} else selected <- as.character(selected)
if (!is.null(size) && selectize) {
stop("'size' argument is incompatible with 'selectize=TRUE'.")
@@ -133,7 +155,7 @@ needOptgroup <- function(choices) {
#' @rdname selectInput
#' @param ... Arguments passed to \code{selectInput()}.
#' @param options A list of options. See the documentation of \pkg{selectize.js}
#' for possible options (character option values inside \code{\link{I}()} will
#' for possible options (character option values inside \code{\link[base]{I}()} will
#' be treated as literal JavaScript code; see \code{\link{renderDataTable}()}
#' for details).
#' @param width The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
@@ -172,7 +194,7 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
if ('drag_drop' %in% options$plugins) {
selectizeDep <- list(selectizeDep, htmlDependency(
'jqueryui', '1.11.4', c(href = 'shared/jqueryui'),
'jqueryui', '1.12.1', c(href = 'shared/jqueryui'),
script = 'jquery-ui.min.js'
))
}
@@ -190,3 +212,135 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
attachDependencies(select, selectizeDep)
}
#' Select variables from a data frame
#'
#' Create a select list that can be used to choose a single or multiple items
#' from the column names of a data frame.
#'
#' The resulting server \code{input} value will be returned as:
#' \itemize{
#' \item a symbol if \code{multiple = FALSE}. The \code{input} value should be
#' used with rlang's \code{\link[rlang]{!!}}. For example,
#' \code{ggplot2::aes(!!input$variable)}.
#' \item a list of symbols if \code{multiple = TRUE}. The \code{input} value
#' should be used with rlang's \code{\link[rlang]{!!!}} to expand
#' the symbol list as individual arguments. For example,
#' \code{dplyr::select(mtcars, !!!input$variabls)} which is
#' equivalent to \code{dplyr::select(mtcars, !!input$variabls[[1]], !!input$variabls[[2]], ..., !!input$variabls[[length(input$variabls)]])}.
#' }
#'
#' By default, \code{varSelectInput()} and \code{selectizeInput()} use the
#' JavaScript library \pkg{selectize.js}
#' (\url{https://github.com/selectize/selectize.js}) to instead of the basic
#' select input element. To use the standard HTML select input element, use
#' \code{selectInput()} with \code{selectize=FALSE}.
#'
#' @inheritParams selectInput
#' @param data A data frame. Used to retrieve the column names as choices for a \code{\link{selectInput}}
#' @return A variable select list control that can be added to a UI definition.
#'
#' @family input elements
#' @seealso \code{\link{updateSelectInput}}
#' @examples
#'
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
#'
#' library(ggplot2)
#'
#' # single selection
#' shinyApp(
#' ui = fluidPage(
#' varSelectInput("variable", "Variable:", mtcars),
#' plotOutput("data")
#' ),
#' server = function(input, output) {
#' output$data <- renderPlot({
#' ggplot(mtcars, aes(!!input$variable)) + geom_histogram()
#' })
#' }
#' )
#'
#'
#' # multiple selections
#' \dontrun{
#' shinyApp(
#' ui = fluidPage(
#' varSelectInput("variables", "Variable:", mtcars, multiple = TRUE),
#' tableOutput("data")
#' ),
#' server = function(input, output) {
#' output$data <- renderTable({
#' if (length(input$variables) == 0) return(mtcars)
#' mtcars %>% dplyr::select(!!!input$variables)
#' }, rownames = TRUE)
#' }
#' )}
#'
#' }
#' @export
varSelectInput <- function(
inputId, label, data, selected = NULL,
multiple = FALSE, selectize = TRUE, width = NULL,
size = NULL
) {
# no place holders
choices <- colnames(data)
selectInputVal <- selectInput(
inputId = inputId,
label = label,
choices = choices,
selected = selected,
multiple = multiple,
selectize = selectize,
width = width,
size = size
)
# set the select tag class to be "symbol"
selectClass <- selectInputVal$children[[2]]$children[[1]]$attribs$class
if (is.null(selectClass)) {
newClass <- "symbol"
} else {
newClass <- paste(selectClass, "symbol", sep = " ")
}
selectInputVal$children[[2]]$children[[1]]$attribs$class <- newClass
selectInputVal
}
#' @rdname varSelectInput
#' @param ... Arguments passed to \code{varSelectInput()}.
#' @param options A list of options. See the documentation of \pkg{selectize.js}
#' for possible options (character option values inside \code{\link[base]{I}()} will
#' be treated as literal JavaScript code; see \code{\link{renderDataTable}()}
#' for details).
#' @param width The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
#' see \code{\link{validateCssUnit}}.
#' @note The variable selectize input created from \code{varSelectizeInput()} allows
#' deletion of the selected option even in a single select input, which will
#' return an empty string as its value. This is the default behavior of
#' \pkg{selectize.js}. However, the selectize input created from
#' \code{selectInput(..., selectize = TRUE)} will ignore the empty string
#' value when it is a single choice input and the empty string is not in the
#' \code{choices} argument. This is to keep compatibility with
#' \code{selectInput(..., selectize = FALSE)}.
#' @export
varSelectizeInput <- function(inputId, ..., options = NULL, width = NULL) {
selectizeIt(
inputId,
varSelectInput(inputId, ..., selectize = FALSE, width = width),
options
)
}

View File

@@ -36,7 +36,7 @@
#' format string, to be passed to the Javascript strftime library. See
#' \url{https://github.com/samsonjs/strftime} for more details. The allowed
#' format specifications are very similar, but not identical, to those for R's
#' \code{\link{strftime}} function. For Dates, the default is \code{"\%F"}
#' \code{\link[base]{strftime}} function. For Dates, the default is \code{"\%F"}
#' (like \code{"2015-07-01"}), and for POSIXt, the default is \code{"\%F \%T"}
#' (like \code{"2015-07-01 15:32:10"}).
#' @param timezone Only used if the values are POSIXt objects. A string
@@ -51,6 +51,7 @@
#' @examples
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
#' options(device.ask.default = FALSE)
#'
#' ui <- fluidPage(
#' sliderInput("obs", "Number of observations:",
@@ -85,40 +86,25 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
version = "0.10.2.2")
}
value <- restoreInput(id = inputId, default = value)
dataType <- getSliderType(min, max, value)
# If step is NULL, use heuristic to set the step size.
findStepSize <- function(min, max, step) {
if (!is.null(step)) return(step)
range <- max - min
# If short range or decimals, use continuous decimal with ~100 points
if (range < 2 || hasDecimals(min) || hasDecimals(max)) {
step <- pretty(c(min, max), n = 100)
step[2] - step[1]
} else {
1
}
if (is.null(timeFormat)) {
timeFormat <- switch(dataType, date = "%F", datetime = "%F %T", number = NULL)
}
if (inherits(min, "Date")) {
if (!inherits(max, "Date") || !inherits(value, "Date"))
stop("`min`, `max`, and `value must all be Date or non-Date objects")
dataType <- "date"
# Restore bookmarked values here, after doing the type checking, because the
# restored value will be a character vector instead of Date or POSIXct, and we can do
# the conversion to correct type next.
value <- restoreInput(id = inputId, default = value)
if (is.null(timeFormat))
timeFormat <- "%F"
} else if (inherits(min, "POSIXt")) {
if (!inherits(max, "POSIXt") || !inherits(value, "POSIXt"))
stop("`min`, `max`, and `value must all be POSIXt or non-POSIXt objects")
dataType <- "datetime"
if (is.null(timeFormat))
timeFormat <- "%F %T"
} else {
dataType <- "number"
if (is.character(value)) {
# If we got here, the value was restored from a URL-encoded bookmark.
if (dataType == "date") {
value <- as.Date(value, format = "%Y-%m-%d")
} else if (dataType == "datetime") {
# Date-times will have a format like "2018-02-28T03:46:26Z"
value <- as.POSIXct(value, format = "%Y-%m-%dT%H:%M:%SZ", tz = "UTC")
}
}
step <- findStepSize(min, max, step)
@@ -164,11 +150,13 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
`data-grid-num` = n_ticks,
`data-grid-snap` = FALSE,
`data-prettify-separator` = sep,
`data-prettify-enabled` = (sep != ""),
`data-prefix` = pre,
`data-postfix` = post,
`data-keyboard` = TRUE,
`data-keyboard-step` = step / (max - min) * 100,
`data-drag-interval` = dragRange,
# This value is only relevant for range sliders; for non-range sliders it
# causes problems since ion.RangeSlider 2.1.2 (issue #1605).
`data-drag-interval` = if (length(value) > 1) dragRange,
# The following are ignored by the ion.rangeSlider, but are used by Shiny.
`data-data-type` = dataType,
`data-time-format` = timeFormat,
@@ -214,7 +202,7 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
}
dep <- list(
htmlDependency("ionrangeslider", "2.1.2", c(href="shared/ionrangeslider"),
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.
@@ -234,6 +222,34 @@ 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)
range <- max - min
# If short range or decimals, use continuous decimal with ~100 points
if (range < 2 || hasDecimals(min) || hasDecimals(max)) {
# Workaround for rounding errors (#1006): the intervals between the items
# returned by pretty() can have rounding errors. To avoid this, we'll use
# pretty() to find the min, max, and number of steps, and then use those
# values to calculate the step size.
pretty_steps <- pretty(c(min, max), n = 100)
n_steps <- length(pretty_steps) - 1
# Fix for #2061: Windows has low-significance digits (like 17 digits out)
# even at the boundaries of pretty()'s output. Use signif(digits = 10),
# which should be way way less significant than any data we'd want to keep.
# It might make sense to use signif(steps[2] - steps[1], 10) instead, but
# for now trying to make the minimal change.
signif(digits = 10, (max(pretty_steps) - min(pretty_steps)) / n_steps)
} else {
1
}
}
#' @rdname sliderInput
#'
#' @param interval The interval, in milliseconds, between each animation step.

View File

@@ -1,8 +1,27 @@
#' Create a submit button
#'
#' Create a submit button for an input form. Forms that include a submit
#' Create a submit button for an app. Apps that include a submit
#' button do not automatically update their outputs when inputs change,
#' rather they wait until the user explicitly clicks the submit button.
#' The use of \code{submitButton} is generally discouraged in favor of
#' the more versatile \code{\link{actionButton}} (see details below).
#'
#' Submit buttons are unusual Shiny inputs, and we recommend using
#' \code{\link{actionButton}} instead of \code{submitButton} when you
#' want to delay a reaction.
#' See \href{http://shiny.rstudio.com/articles/action-buttons.html}{this
#' article} for more information (including a demo of how to "translate"
#' code using a \code{submitButton} to code using an \code{actionButton}).
#'
#' In essence, the presence of a submit button stops all inputs from
#' sending their values automatically to the server. This means, for
#' instance, that if there are \emph{two} submit buttons in the same app,
#' clicking either one will cause all inputs in the app to send their
#' values to the server. This is probably not what you'd want, which is
#' why submit button are unwieldy for all but the simplest apps. There
#' are other problems with submit buttons: for example, dynamically
#' created submit buttons (for example, with \code{\link{renderUI}}
#' or \code{\link{insertUI}}) will not work.
#'
#' @param text Button caption
#' @param icon Optional \code{\link{icon}} to appear on the button
@@ -13,8 +32,26 @@
#' @family input elements
#'
#' @examples
#' submitButton("Update View")
#' submitButton("Update View", icon("refresh"))
#' if (interactive()) {
#'
#' shinyApp(
#' ui = basicPage(
#' numericInput("num", label = "Make changes", value = 1),
#' submitButton("Update View", icon("refresh")),
#' helpText("When you click the button above, you should see",
#' "the output below update to reflect the value you",
#' "entered at the top:"),
#' verbatimTextOutput("value")
#' ),
#' server = function(input, output) {
#'
#' # submit buttons do not have a value of their own,
#' # they control when the app accesses values of other widgets.
#' # input$num is the value of the number widget.
#' output$value <- renderPrint({ input$num })
#' }
#' )
#' }
#' @export
submitButton <- function(text = "Apply Changes", icon = NULL, width = NULL) {
div(

View File

@@ -2,42 +2,62 @@ controlLabel <- function(controlName, label) {
label %AND% tags$label(class = "control-label", `for` = controlName, label)
}
# Before shiny 0.9, `selected` refers to names/labels of `choices`; now it
# refers to values. Below is a function for backward compatibility.
validateSelected <- function(selected, choices, inputId) {
# drop names, otherwise toJSON() keeps them too
selected <- unname(selected)
# if you are using optgroups, you're using shiny > 0.10.0, and you should
# already know that `selected` must be a value instead of a label
if (needOptgroup(choices)) return(selected)
if (is.list(choices)) choices <- unlist(choices)
nms <- names(choices)
# labels and values are identical, no need to validate
if (identical(nms, unname(choices))) return(selected)
# when selected labels instead of values
i <- (selected %in% nms) & !(selected %in% choices)
if (any(i)) {
warnFun <- if (all(i)) {
# replace names with values
selected <- unname(choices[selected])
warning
} else stop # stop when it is ambiguous (some labels == values)
warnFun("'selected' must be the values instead of names of 'choices' ",
"for the input '", inputId, "'")
# This function takes in either a list or vector for `choices` (and
# `choiceNames` and `choiceValues` are passed in as NULL) OR it takes
# in a list or vector for both `choiceNames` and `choiceValues` (and
# `choices` is passed as NULL) and returns a list of two elements:
# - `choiceNames` is a vector or list that holds the options names
# (each element can be arbitrary UI, or simple text)
# - `choiceValues` is a vector or list that holds the options values
# (each element must be simple text)
normalizeChoicesArgs <- function(choices, choiceNames, choiceValues,
mustExist = TRUE) {
# if-else to check that either choices OR (choiceNames + choiceValues)
# were correctly provided
if (is.null(choices)) {
if (is.null(choiceNames) || is.null(choiceValues)) {
if (mustExist) {
stop("Please specify a non-empty vector for `choices` (or, ",
"alternatively, for both `choiceNames` AND `choiceValues`).")
} else {
if (is.null(choiceNames) && is.null(choiceValues)) {
# this is useful when we call this function from `updateInputOptions()`
# in which case, all three `choices`, `choiceNames` and `choiceValues`
# may legitimately be NULL
return(list(choiceNames = NULL, choiceValues = NULL))
} else {
stop("One of `choiceNames` or `choiceValues` was set to ",
"NULL, but either both or none should be NULL.")
}
}
}
if (length(choiceNames) != length(choiceValues)) {
stop("`choiceNames` and `choiceValues` must have the same length.")
}
if (anyNamed(choiceNames) || anyNamed(choiceValues)) {
stop("`choiceNames` and `choiceValues` must not be named.")
}
} else {
if (!is.null(choiceNames) || !is.null(choiceValues)) {
warning("Using `choices` argument; ignoring `choiceNames` and `choiceValues`.")
}
choices <- choicesWithNames(choices) # resolve names if not specified
choiceNames <- names(choices)
choiceValues <- unname(choices)
}
selected
}
return(list(choiceNames = as.list(choiceNames),
choiceValues = as.list(as.character(choiceValues))))
}
# generate options for radio buttons and checkbox groups (type = 'checkbox' or
# 'radio')
generateOptions <- function(inputId, choices, selected, inline, type = 'checkbox') {
generateOptions <- function(inputId, selected, inline, type = 'checkbox',
choiceNames, choiceValues,
session = getDefaultReactiveDomain()) {
# generate a list of <input type=? [checked] />
options <- mapply(
choices, names(choices),
choiceValues, choiceNames,
FUN = function(value, name) {
inputTag <- tags$input(
type = type, name = inputId, value = value
@@ -45,14 +65,18 @@ generateOptions <- function(inputId, choices, selected, inline, type = 'checkbox
if (value %in% selected)
inputTag$attribs$checked <- "checked"
# in case, the options include UI code other than text
# (arbitrary HTML using the tags() function or equivalent)
pd <- processDeps(name, session)
# If inline, there's no wrapper div, and the label needs a class like
# checkbox-inline.
if (inline) {
tags$label(class = paste0(type, "-inline"), inputTag, tags$span(name))
tags$label(class = paste0(type, "-inline"), inputTag,
tags$span(pd$html, pd$deps))
} else {
tags$div(class = type,
tags$label(inputTag, tags$span(name))
)
tags$div(class = type, tags$label(inputTag,
tags$span(pd$html, pd$deps)))
}
},
SIMPLIFY = FALSE, USE.NAMES = FALSE
@@ -63,7 +87,7 @@ generateOptions <- function(inputId, choices, selected, inline, type = 'checkbox
# Takes a vector or list, and adds names (same as the value) to any entries
# without names.
# without names. Coerces all leaf nodes to `character`.
choicesWithNames <- function(choices) {
# Take a vector or list, and convert to list. Also, if any children are
# vectors with length > 1, convert those to list. If the list is unnamed,
@@ -79,7 +103,7 @@ choicesWithNames <- function(choices) {
if (is.list(val))
listify(val)
else if (length(val) == 1 && is.null(names(val)))
val
as.character(val)
else
makeNamed(as.list(val))
})

325
R/insert-tab.R Normal file
View File

@@ -0,0 +1,325 @@
#' Dynamically insert/remove a tabPanel
#'
#' Dynamically insert or remove a \code{\link{tabPanel}} (or a
#' \code{\link{navbarMenu}}) from an existing \code{\link{tabsetPanel}},
#' \code{\link{navlistPanel}} or \code{\link{navbarPage}}.
#'
#' When you want to insert a new tab before or after an existing tab, you
#' should use \code{insertTab}. When you want to prepend a tab (i.e. add a
#' tab to the beginning of the \code{tabsetPanel}), use \code{prependTab}.
#' When you want to append a tab (i.e. add a tab to the end of the
#' \code{tabsetPanel}), use \code{appendTab}.
#'
#' For \code{navbarPage}, you can insert/remove conventional
#' \code{tabPanel}s (whether at the top level or nested inside a
#' \code{navbarMenu}), as well as an entire \code{\link{navbarMenu}}.
#' For the latter case, \code{target} should be the \code{menuName} that
#' you gave your \code{navbarMenu} when you first created it (by default,
#' this is equal to the value of the \code{title} argument).
#'
#' @param inputId The \code{id} of the \code{tabsetPanel} (or
#' \code{navlistPanel} or \code{navbarPage}) into which \code{tab} will
#' be inserted/removed.
#'
#' @param tab The item to be added (must be created with \code{tabPanel},
#' or with \code{navbarMenu}).
#'
#' @param target If inserting: the \code{value} of an existing
#' \code{tabPanel}, next to which \code{tab} will be added.
#' If removing: the \code{value} of the \code{tabPanel} that
#' you want to remove. See Details if you want to insert next to/remove
#' an entire \code{navbarMenu} instead.
#'
#' @param position Should \code{tab} be added before or after the
#' \code{target} tab?
#'
#' @param select Should \code{tab} be selected upon being inserted?
#'
#' @param session The shiny session within which to call this function.
#'
#' @seealso \code{\link{showTab}}
#'
#' @examples
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#'
#' # example app for inserting/removing a tab
#' ui <- fluidPage(
#' sidebarLayout(
#' sidebarPanel(
#' actionButton("add", "Add 'Dynamic' tab"),
#' actionButton("remove", "Remove 'Foo' tab")
#' ),
#' mainPanel(
#' tabsetPanel(id = "tabs",
#' tabPanel("Hello", "This is the hello tab"),
#' tabPanel("Foo", "This is the foo tab"),
#' tabPanel("Bar", "This is the bar tab")
#' )
#' )
#' )
#' )
#' server <- function(input, output, session) {
#' observeEvent(input$add, {
#' insertTab(inputId = "tabs",
#' tabPanel("Dynamic", "This a dynamically-added tab"),
#' target = "Bar"
#' )
#' })
#' observeEvent(input$remove, {
#' removeTab(inputId = "tabs", target = "Foo")
#' })
#' }
#'
#' shinyApp(ui, server)
#'
#'
#' # example app for prepending/appending a navbarMenu
#' ui <- navbarPage("Navbar page", id = "tabs",
#' tabPanel("Home",
#' actionButton("prepend", "Prepend a navbarMenu"),
#' actionButton("append", "Append a navbarMenu")
#' )
#' )
#' server <- function(input, output, session) {
#' observeEvent(input$prepend, {
#' id <- paste0("Dropdown", input$prepend, "p")
#' prependTab(inputId = "tabs",
#' navbarMenu(id,
#' tabPanel("Drop1", paste("Drop1 page from", id)),
#' tabPanel("Drop2", paste("Drop2 page from", id)),
#' "------",
#' "Header",
#' tabPanel("Drop3", paste("Drop3 page from", id))
#' )
#' )
#' })
#' observeEvent(input$append, {
#' id <- paste0("Dropdown", input$append, "a")
#' appendTab(inputId = "tabs",
#' navbarMenu(id,
#' tabPanel("Drop1", paste("Drop1 page from", id)),
#' tabPanel("Drop2", paste("Drop2 page from", id)),
#' "------",
#' "Header",
#' tabPanel("Drop3", paste("Drop3 page from", id))
#' )
#' )
#' })
#' }
#'
#' shinyApp(ui, server)
#'
#' }
#' @export
insertTab <- function(inputId, tab, target,
position = c("before", "after"), select = FALSE,
session = getDefaultReactiveDomain()) {
force(target)
force(select)
position <- match.arg(position)
inputId <- session$ns(inputId)
# Barbara -- August 2017
# Note: until now, the number of tabs in a tabsetPanel (or navbarPage
# or navlistPanel) was always fixed. So, an easy way to give an id to
# a tab was simply incrementing a counter. (Just like it was easy to
# give a random 4-digit number to identify the tabsetPanel). Since we
# can only know this in the client side, we'll just pass `id` and
# `tsid` (TabSetID) as dummy values that will be fixed in the JS code.
item <- buildTabItem("id", "tsid", TRUE, divTag = tab,
textFilter = if (is.character(tab)) navbarMenuTextFilter else NULL)
callback <- function() {
session$sendInsertTab(
inputId = inputId,
liTag = processDeps(item$liTag, session),
divTag = processDeps(item$divTag, session),
menuName = NULL,
target = target,
position = position,
select = select)
}
session$onFlush(callback, once = TRUE)
}
#' @param menuName This argument should only be used when you want to
#' prepend (or append) \code{tab} to the beginning (or end) of an
#' existing \code{\link{navbarMenu}} (which must itself be part of
#' an existing \code{\link{navbarPage}}). In this case, this argument
#' should be the \code{menuName} that you gave your \code{navbarMenu}
#' when you first created it (by default, this is equal to the value
#' of the \code{title} argument). Note that you still need to set the
#' \code{inputId} argument to whatever the \code{id} of the parent
#' \code{navbarPage} is. If \code{menuName} is left as \code{NULL},
#' \code{tab} will be prepended (or appended) to whatever
#' \code{inputId} is.
#'
#' @rdname insertTab
#' @export
prependTab <- function(inputId, tab, select = FALSE, menuName = NULL,
session = getDefaultReactiveDomain()) {
force(select)
force(menuName)
inputId <- session$ns(inputId)
item <- buildTabItem("id", "tsid", TRUE, divTag = tab,
textFilter = if (is.character(tab)) navbarMenuTextFilter else NULL)
callback <- function() {
session$sendInsertTab(
inputId = inputId,
liTag = processDeps(item$liTag, session),
divTag = processDeps(item$divTag, session),
menuName = menuName,
target = NULL,
position = "after",
select = select)
}
session$onFlush(callback, once = TRUE)
}
#' @rdname insertTab
#' @export
appendTab <- function(inputId, tab, select = FALSE, menuName = NULL,
session = getDefaultReactiveDomain()) {
force(select)
force(menuName)
inputId <- session$ns(inputId)
item <- buildTabItem("id", "tsid", TRUE, divTag = tab,
textFilter = if (is.character(tab)) navbarMenuTextFilter else NULL)
callback <- function() {
session$sendInsertTab(
inputId = inputId,
liTag = processDeps(item$liTag, session),
divTag = processDeps(item$divTag, session),
menuName = menuName,
target = NULL,
position = "before",
select = select)
}
session$onFlush(callback, once = TRUE)
}
#' @rdname insertTab
#' @export
removeTab <- function(inputId, target,
session = getDefaultReactiveDomain()) {
force(target)
inputId <- session$ns(inputId)
callback <- function() {
session$sendRemoveTab(
inputId = inputId,
target = target)
}
session$onFlush(callback, once = TRUE)
}
#' Dynamically hide/show a tabPanel
#'
#' Dynamically hide or show a \code{\link{tabPanel}} (or a
#' \code{\link{navbarMenu}})from an existing \code{\link{tabsetPanel}},
#' \code{\link{navlistPanel}} or \code{\link{navbarPage}}.
#'
#' For \code{navbarPage}, you can hide/show conventional
#' \code{tabPanel}s (whether at the top level or nested inside a
#' \code{navbarMenu}), as well as an entire \code{\link{navbarMenu}}.
#' For the latter case, \code{target} should be the \code{menuName} that
#' you gave your \code{navbarMenu} when you first created it (by default,
#' this is equal to the value of the \code{title} argument).
#'
#' @param inputId The \code{id} of the \code{tabsetPanel} (or
#' \code{navlistPanel} or \code{navbarPage}) in which to find
#' \code{target}.
#'
#' @param target The \code{value} of the \code{tabPanel} to be
#' hidden/shown. See Details if you want to hide/show an entire
#' \code{navbarMenu} instead.
#'
#' @param select Should \code{target} be selected upon being shown?
#'
#' @param session The shiny session within which to call this function.
#'
#' @seealso \code{\link{insertTab}}
#'
#' @examples
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#'
#' ui <- navbarPage("Navbar page", id = "tabs",
#' tabPanel("Home",
#' actionButton("hideTab", "Hide 'Foo' tab"),
#' actionButton("showTab", "Show 'Foo' tab"),
#' actionButton("hideMenu", "Hide 'More' navbarMenu"),
#' actionButton("showMenu", "Show 'More' navbarMenu")
#' ),
#' tabPanel("Foo", "This is the foo tab"),
#' tabPanel("Bar", "This is the bar tab"),
#' navbarMenu("More",
#' tabPanel("Table", "Table page"),
#' tabPanel("About", "About page"),
#' "------",
#' "Even more!",
#' tabPanel("Email", "Email page")
#' )
#' )
#'
#' server <- function(input, output, session) {
#' observeEvent(input$hideTab, {
#' hideTab(inputId = "tabs", target = "Foo")
#' })
#'
#' observeEvent(input$showTab, {
#' showTab(inputId = "tabs", target = "Foo")
#' })
#'
#' observeEvent(input$hideMenu, {
#' hideTab(inputId = "tabs", target = "More")
#' })
#'
#' observeEvent(input$showMenu, {
#' showTab(inputId = "tabs", target = "More")
#' })
#' }
#'
#' shinyApp(ui, server)
#' }
#'
#' @export
showTab <- function(inputId, target, select = FALSE,
session = getDefaultReactiveDomain()) {
force(target)
if (select) updateTabsetPanel(session, inputId, selected = target)
inputId <- session$ns(inputId)
callback <- function() {
session$sendChangeTabVisibility(
inputId = inputId,
target = target,
type = "show"
)
}
session$onFlush(callback, once = TRUE)
}
#' @rdname showTab
#' @export
hideTab <- function(inputId, target,
session = getDefaultReactiveDomain()) {
force(target)
inputId <- session$ns(inputId)
callback <- function() {
session$sendChangeTabVisibility(
inputId = inputId,
target = target,
type = "hide"
)
}
session$onFlush(callback, once = TRUE)
}

View File

@@ -79,8 +79,6 @@ absolutePanel <- function(...,
if (isTRUE(draggable)) {
divTag <- tagAppendAttributes(divTag, class='draggable')
return(tagList(
# IMPORTANT NOTE: If you update jqueryui, make sure you DON'T include the datepicker,
# as it collides with our bootstrap datepicker!
singleton(tags$head(tags$script(src='shared/jqueryui/jquery-ui.min.js'))),
divTag,
tags$script('$(".draggable").draggable();')

View File

@@ -191,7 +191,7 @@ staticHandler <- function(root) {
if (!identical(req$REQUEST_METHOD, 'GET'))
return(NULL)
path <- req$PATH_INFO
path <- URLdecode(req$PATH_INFO)
if (is.null(path))
return(httpResponse(400, content="<h1>Bad Request</h1>"))
@@ -351,38 +351,72 @@ HandlerManager <- R6Class("HandlerManager",
}
response <- handler(req)
if (is.null(response))
response <- httpResponse(404, content="<h1>Not Found</h1>")
if (inherits(response, "httpResponse")) {
headers <- as.list(response$headers)
headers$'Content-Type' <- response$content_type
res <- hybrid_chain(response, function(response) {
if (is.null(response))
response <- httpResponse(404, content="<h1>Not Found</h1>")
if (inherits(response, "httpResponse")) {
headers <- as.list(response$headers)
headers$'Content-Type' <- response$content_type
response <- filter(req, response)
if (head_request) {
headers$`Content-Length` <- getResponseContentLength(response, deleteOwnedContent = TRUE)
return(list(
status = response$status,
body = "",
headers = headers
))
} else {
return(list(
status = response$status,
body = response$content,
headers = headers
))
}
response <- filter(req, response)
if (head_request) {
headers$`Content-Length` <- nchar(response$content, type = "bytes")
return(list(
status = response$status,
body = "",
headers = headers
))
} else {
return(list(
status = response$status,
body = response$content,
headers = headers
))
# Assume it's a Rook-compatible response
return(response)
}
} else {
# Assume it's a Rook-compatible response
return(response)
}
})
}
}
)
)
# 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
# content that is of the form list(file=..., owned=TRUE).
getResponseContentLength <- function(response, deleteOwnedContent) {
force(deleteOwnedContent)
result <- if (is.character(response$content) && length(response$content) == 1) {
nchar(response$content, type = "bytes")
} else if (is.raw(response$content)) {
length(response$content)
} else if (is.list(response$content) && !is.null(response$content$file)) {
if (deleteOwnedContent && isTRUE(response$content$owned)) {
on.exit(unlink(response$content$file, recursive = FALSE, force = FALSE), add = TRUE)
}
file.info(response$content$file)$size
} else {
warning("HEAD request for unexpected content class ", class(response$content)[[1]])
NULL
}
if (is.na(result)) {
# Mostly for missing file case
return(NULL)
} else {
return(result)
}
}
#
# ## Next steps
#

View File

@@ -43,6 +43,8 @@ removeModal <- function(session = getDefaultReactiveDomain()) {
#' \code{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
#' from a call to \code{\link{removeModal}} on the server.
#' @param fade If \code{FALSE}, the modal dialog will have no fade-in animation
#' (it will simply appear rather than fade in to view).
#'
#' @examples
#' if (interactive()) {
@@ -143,11 +145,12 @@ removeModal <- function(session = getDefaultReactiveDomain()) {
#' }
#' @export
modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"),
size = c("m", "s", "l"), easyClose = FALSE) {
size = c("m", "s", "l"), easyClose = FALSE, fade = TRUE) {
size <- match.arg(size)
div(id = "shiny-modal", class = "modal fade", tabindex = "-1",
cls <- if (fade) "modal fade" else "modal"
div(id = "shiny-modal", class = cls, tabindex = "-1",
`data-backdrop` = if (!easyClose) "static",
`data-keyboard` = if (!easyClose) "false",

View File

@@ -26,6 +26,11 @@ createSessionProxy <- function(parentSession, ...) {
#' @export
`$<-.session_proxy` <- function(x, name, value) {
# this line allows users to write into session$userData
# (e.g. it allows something like `session$userData$x <- TRUE`,
# but not `session$userData <- TRUE`) from within a module
# without any hacks (see PR #1732)
if (identical(x[[name]], value)) return(x)
stop("Attempted to assign value on session proxy.")
}

View File

@@ -55,7 +55,6 @@
#' de-emphasized appearance relative to \code{message}.
#' @param value A numeric value at which to set
#' the progress bar, relative to \code{min} and \code{max}.
#' \code{NULL} hides the progress bar, if it is currently visible.
#' @param style Progress display style. If \code{"notification"} (the default),
#' the progress indicator will show using Shiny's notification API. If
#' \code{"old"}, use the same HTML and CSS used in Shiny 0.13.2 and below
@@ -98,7 +97,6 @@
#' @export
Progress <- R6Class(
'Progress',
portable = TRUE,
public = list(
initialize = function(session = getDefaultReactiveDomain(),
@@ -112,8 +110,8 @@ Progress <- R6Class(
private$id <- createUniqueId(8)
private$min <- min
private$max <- max
private$style <- match.arg(style, choices = c("notification", "old"))
private$value <- NULL
private$style <- match.arg(style, choices = c("notification", "old"))
private$closed <- FALSE
session$sendProgress('open', list(id = private$id, style = private$style))
@@ -125,15 +123,15 @@ Progress <- R6Class(
return()
}
if (is.null(value) || is.na(value)) {
if (is.null(value) || is.na(value))
value <- NULL
} else {
if (!is.null(value)) {
private$value <- value
# Normalize value to number between 0 and 1
value <- min(1, max(0, (value - private$min) / (private$max - private$min)))
}
private$value <- value
data <- dropNulls(list(
id = private$id,
message = message,
@@ -142,11 +140,14 @@ Progress <- R6Class(
style = private$style
))
private$session$sendProgress('update', data)
private$session$sendProgress('update', data)
},
inc = function(amount = 0.1, message = NULL, detail = NULL) {
value <- min(self$getValue() + amount, private$max)
if (is.null(private$value))
private$value <- private$min
value <- min(private$value + amount, private$max)
self$set(value, message, detail)
},
@@ -154,10 +155,7 @@ Progress <- R6Class(
getMax = function() private$max,
# Return value (not the normalized 0-1 value, but in the original range)
getValue = function() {
private$value * (private$max - private$min) + private$min
},
getValue = function() private$value,
close = function() {
if (private$closed) {
@@ -173,12 +171,12 @@ Progress <- R6Class(
),
private = list(
session = 'environment',
session = 'ShinySession',
id = character(0),
min = numeric(0),
max = numeric(0),
style = character(0),
value = NULL,
value = numeric(0),
closed = logical(0)
)
)
@@ -239,12 +237,12 @@ Progress <- R6Class(
#' \code{"old"}, use the same HTML and CSS used in Shiny 0.13.2 and below
#' (this is for backward-compatibility).
#' @param value Single-element numeric vector; the value at which to set the
#' progress bar, relative to \code{min} and \code{max}. \code{NULL} hides the
#' progress bar, if it is currently visible.
#' progress bar, relative to \code{min} and \code{max}.
#'
#' @examples
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
#' options(device.ask.default = FALSE)
#'
#' ui <- fluidPage(
#' plotOutput("plot")

View File

@@ -1,3 +1,21 @@
processId <- local({
# pid is not sufficient to uniquely identify a process, because
# distributed futures span machines which could introduce pid
# collisions.
cached <- NULL
function() {
if (is.null(cached)) {
cached <<- digest::digest(list(
Sys.info(),
Sys.time()
))
}
# Sys.getpid() cannot be cached because forked children will
# then have the same processId as their parents.
paste(cached, Sys.getpid())
}
})
Context <- R6Class(
'Context',
portable = FALSE,
@@ -9,25 +27,37 @@ Context <- R6Class(
.invalidateCallbacks = list(),
.flushCallbacks = list(),
.domain = NULL,
.pid = NULL,
initialize = function(domain, label='', type='other', prevId='') {
id <<- .getReactiveEnvironment()$nextId()
.label <<- label
.domain <<- domain
.pid <<- processId()
.graphCreateContext(id, label, type, prevId, domain)
},
run = function(func) {
"Run the provided function under this context."
withReactiveDomain(.domain, {
env <- .getReactiveEnvironment()
.graphEnterContext(id)
on.exit(.graphExitContext(id), add = TRUE)
env$runWith(self, func)
promises::with_promise_domain(reactivePromiseDomain(), {
withReactiveDomain(.domain, {
env <- .getReactiveEnvironment()
.graphEnterContext(id)
on.exit({
.graphExitContext(id, domain = .domain)
}, add = TRUE)
env$runWith(self, func)
})
})
},
invalidate = function() {
"Invalidate this context. It will immediately call the callbacks
that have been registered with onInvalidate()."
if (!identical(.pid, processId())) {
stop("Reactive context was created in one process and invalidated from another")
}
if (.invalidated)
return()
.invalidated <<- TRUE
@@ -43,6 +73,11 @@ Context <- R6Class(
"Register a function to be called when this context is invalidated.
If this context is already invalidated, the function is called
immediately."
if (!identical(.pid, processId())) {
stop("Reactive context was created in one process and accessed from another")
}
if (.invalidated)
func()
else
@@ -52,9 +87,6 @@ Context <- R6Class(
addPendingFlush = function(priority) {
"Tell the reactive environment that this context should be flushed the
next time flushReact() called."
if (!is.null(.domain)) {
.domain$incrementBusyCount()
}
.getReactiveEnvironment()$addPendingFlush(self, priority)
},
onFlush = function(func) {
@@ -64,12 +96,6 @@ Context <- R6Class(
executeFlushCallbacks = function() {
"For internal use only."
on.exit({
if (!is.null(.domain)) {
.domain$decrementBusyCount()
}
}, add = TRUE)
lapply(.flushCallbacks, function(flushCallback) {
flushCallback()
})
@@ -118,9 +144,12 @@ ReactiveEnvironment <- R6Class(
hasPendingFlush = function() {
return(!.pendingFlush$isEmpty())
},
# Returns TRUE if anything was actually called
flush = function() {
# If nothing to flush, exit early
if (!hasPendingFlush()) return(invisible(FALSE))
# If already in a flush, don't start another one
if (.inFlush) return()
if (.inFlush) return(invisible(FALSE))
.inFlush <<- TRUE
on.exit(.inFlush <<- FALSE)
@@ -128,6 +157,8 @@ ReactiveEnvironment <- R6Class(
ctx <- .pendingFlush$dequeue()
ctx$executeFlushCallbacks()
}
invisible(TRUE)
}
)
)
@@ -141,9 +172,10 @@ ReactiveEnvironment <- R6Class(
}
})
# Causes any pending invalidations to run.
# Causes any pending invalidations to run. Returns TRUE if any invalidations
# were pending (i.e. if work was actually done).
flushReact <- function() {
.getReactiveEnvironment()$flush()
return(.getReactiveEnvironment()$flush())
}
# Retrieves the current reactive context, or errors if there is no reactive
@@ -163,3 +195,31 @@ local({
return(dummyContext)
}
})
wrapForContext <- function(func, ctx) {
force(func)
force(ctx)
function(...) {
ctx$run(function() {
captureStackTraces(
func(...)
)
})
}
}
reactivePromiseDomain <- function() {
promises::new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
ctx <- getCurrentContext()
wrapForContext(onFulfilled, ctx)
},
wrapOnRejected = function(onRejected) {
force(onRejected)
ctx <- getCurrentContext()
wrapForContext(onRejected, ctx)
}
)
}

View File

@@ -95,11 +95,7 @@ getDefaultReactiveDomain <- function() {
#' @rdname domains
#' @export
withReactiveDomain <- function(domain, expr) {
oldValue <- .globals$domain
.globals$domain <- domain
on.exit(.globals$domain <- oldValue)
expr
promises::with_promise_domain(createVarPromiseDomain(.globals, "domain", domain), expr)
}
#

File diff suppressed because it is too large Load Diff

588
R/render-cached-plot.R Normal file
View File

@@ -0,0 +1,588 @@
#' Plot output with cached images
#'
#' Renders a reactive plot, with plot images cached to disk.
#'
#' \code{expr} is an expression that generates a plot, similar to that in
#' \code{renderPlot}. Unlike with \code{renderPlot}, this expression does not
#' take reactive dependencies. It is re-executed only when the cache key
#' changes.
#'
#' \code{cacheKeyExpr} is an expression which, when evaluated, returns an object
#' which will be serialized and hashed using the \code{\link[digest]{digest}}
#' function to generate a string that will be used as a cache key. This key is
#' used to identify the contents of the plot: if the cache key is the same as a
#' previous time, it assumes that the plot is the same and can be retrieved from
#' the cache.
#'
#' This \code{cacheKeyExpr} is reactive, and so it will be re-evaluated when any
#' upstream reactives are invalidated. This will also trigger re-execution of
#' the plotting expression, \code{expr}.
#'
#' The key should consist of "normal" R objects, like vectors and lists. Lists
#' should in turn contain other normal R objects. If the key contains
#' environments, external pointers, or reference objects -- or even if it has
#' such objects attached as attributes -- then it is possible that it will
#' change unpredictably even when you do not expect it to. Additionally, because
#' the entire key is serialized and hashed, if it contains a very large object
#' -- a large data set, for example -- there may be a noticeable performance
#' penalty.
#'
#' If you face these issues with the cache key, you can work around them by
#' extracting out the important parts of the objects, and/or by converting them
#' to normal R objects before returning them. Your expression could even
#' serialize and hash that information in an efficient way and return a string,
#' which will in turn be hashed (very quickly) by the
#' \code{\link[digest]{digest}} function.
#'
#' Internally, the result from \code{cacheKeyExpr} is combined with the name of
#' the output (if you assign it to \code{output$plot1}, it will be combined
#' with \code{"plot1"}) to form the actual key that is used. As a result, even
#' if there are multiple plots that have the same \code{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 \code{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 \code{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 \code{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 \code{"app"} or \code{"session"} is used, the cache will be 10 MB
#' in size, and will be stored stored in memory, using a
#' \code{\link{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
#' \code{\link{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
#' \code{\link{shinyOptions}()} at the top of your server function. To use
#' the session-scoped cache, you must also call \code{renderCachedPlot} with
#' \code{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 \code{\link{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
#' \code{myapp-cache} (replace \code{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 \code{\link{memoryCache}} or \code{\link{diskCache}}, and pass it
#' as the \code{cache} argument of \code{renderCachedPlot}.
#'
#' @section Interactive plots:
#'
#' \code{renderCachedPlot} can be used to create interactive plots. See
#' \code{\link{plotOutput}} for more information and examples.
#'
#'
#' @inheritParams renderPlot
#' @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.
#' @param sizePolicy A function that takes two arguments, \code{width} and
#' \code{height}, and returns a list with \code{width} and \code{height}. 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 \code{\link{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
#' \code{"app"} (the default), \code{"session"}, or a cache object like
#' a \code{\link{diskCache}}. See the Cache Scoping section for more
#' information.
#'
#' @seealso See \code{\link{renderPlot}} for the regular, non-cached version of
#' this function. For more about configuring caches, see
#' \code{\link{memoryCache}} and \code{\link{diskCache}}.
#'
#'
#' @examples
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
#'
#' # A basic example that uses the default app-scoped memory cache.
#' # The cache will be shared among all simultaneous users of the application.
#' shinyApp(
#' fluidPage(
#' sidebarLayout(
#' sidebarPanel(
#' sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
#' ),
#' mainPanel(plotOutput("plot"))
#' )
#' ),
#' function(input, output, session) {
#' output$plot <- renderCachedPlot({
#' Sys.sleep(2) # Add an artificial delay
#' seqn <- seq_len(input$n)
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
#' },
#' cacheKeyExpr = { list(input$n) }
#' )
#' }
#' )
#'
#'
#'
#' # An example uses a data object shared across sessions. mydata() is part of
#' # the cache key, so when its value changes, plots that were previously
#' # stored in the cache will no longer be used (unless mydata() changes back
#' # to its previous value).
#' mydata <- reactiveVal(data.frame(x = rnorm(400), y = rnorm(400)))
#'
#' ui <- fluidPage(
#' sidebarLayout(
#' sidebarPanel(
#' sliderInput("n", "Number of points", 50, 400, 100, step = 50),
#' actionButton("newdata", "New data")
#' ),
#' mainPanel(
#' plotOutput("plot")
#' )
#' )
#' )
#'
#' server <- function(input, output, session) {
#' observeEvent(input$newdata, {
#' mydata(data.frame(x = rnorm(400), y = rnorm(400)))
#' })
#'
#' output$plot <- renderCachedPlot(
#' {
#' Sys.sleep(2)
#' d <- mydata()
#' seqn <- seq_len(input$n)
#' plot(d$x[seqn], d$y[seqn], xlim = range(d$x), ylim = range(d$y))
#' },
#' cacheKeyExpr = { list(input$n, mydata()) },
#' )
#' }
#'
#' shinyApp(ui, server)
#'
#'
#' # A basic application with two plots, where each plot in each session has
#' # a separate cache.
#' shinyApp(
#' fluidPage(
#' sidebarLayout(
#' sidebarPanel(
#' sliderInput("n", "Number of points", 4, 32, value = 8, step = 4)
#' ),
#' mainPanel(
#' plotOutput("plot1"),
#' plotOutput("plot2")
#' )
#' )
#' ),
#' function(input, output, session) {
#' output$plot1 <- renderCachedPlot({
#' Sys.sleep(2) # Add an artificial delay
#' seqn <- seq_len(input$n)
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
#' },
#' cacheKeyExpr = { list(input$n) },
#' cache = memoryCache()
#' )
#' output$plot2 <- renderCachedPlot({
#' Sys.sleep(2) # Add an artificial delay
#' seqn <- seq_len(input$n)
#' plot(mtcars$wt[seqn], mtcars$mpg[seqn],
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
#' },
#' cacheKeyExpr = { list(input$n) },
#' cache = memoryCache()
#' )
#' }
#' )
#'
#' }
#'
#' \dontrun{
#' # 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))
#'
#' # 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"))
#'
#' # 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"))
#'
#' # 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))
#'
#' output$plot <- renderCachedPlot(
#' ...,
#' cache = "session"
#' )
#' }
#'
#' }
#' @export
renderCachedPlot <- function(expr,
cacheKeyExpr,
sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2),
res = 72,
cache = "app",
...,
outputArgs = list()
) {
# 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(...)
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`.')
}
}
# 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())
}
# 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)
}
)
})
# 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(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)
}
#' Create a sizing function that grows at a given ratio
#'
#' Returns a function which takes a two-element vector representing an input
#' width and height, and returns a two-element vector of width and height. The
#' possible widths are the base width times the growthRate to any integer power.
#' For example, with a base width of 500 and growth rate of 1.25, the possible
#' widths include 320, 400, 500, 625, 782, and so on, both smaller and larger.
#' Sizes are rounded up to the next pixel. Heights are computed the same way as
#' widths.
#'
#' @param width,height Base width and height.
#' @param growthRate Growth rate multiplier.
#'
#' @seealso This is to be used with \code{\link{renderCachedPlot}}.
#'
#' @examples
#' f <- sizeGrowthRatio(500, 500, 1.25)
#' f(c(400, 400))
#' f(c(500, 500))
#' f(c(530, 550))
#' f(c(625, 700))
#'
#' @export
sizeGrowthRatio <- function(width = 400, height = 400, growthRate = 1.2) {
round_dim_up <- function(x, base, rate) {
power <- ceiling(log(x / base, rate))
ceiling(base * rate^power)
}
function(dims) {
if (length(dims) != 2) {
stop("dims must be a vector with two numbers, for width and height.")
}
c(
round_dim_up(dims[1], width, growthRate),
round_dim_up(dims[2], height, growthRate)
)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -81,143 +81,148 @@ renderTable <- function(expr, striped = FALSE, hover = FALSE,
dots <- list(...) ## used later (but defined here because of scoping)
renderFunc <- function(shinysession, name, ...) {
striped <- stripedWrapper()
hover <- hoverWrapper()
bordered <- borderedWrapper()
format <- c(striped = striped, hover = hover, bordered = bordered)
spacing <- spacingWrapper()
width <- widthWrapper()
align <- alignWrapper()
rownames <- rownamesWrapper()
colnames <- colnamesWrapper()
digits <- digitsWrapper()
na <- naWrapper()
createRenderFunction(
func,
function(data, session, name, ...) {
striped <- stripedWrapper()
hover <- hoverWrapper()
bordered <- borderedWrapper()
format <- c(striped = striped, hover = hover, bordered = bordered)
spacing <- spacingWrapper()
width <- widthWrapper()
align <- alignWrapper()
rownames <- rownamesWrapper()
colnames <- colnamesWrapper()
digits <- digitsWrapper()
na <- naWrapper()
spacing_choices <- c("s", "xs", "m", "l")
if (!(spacing %in% spacing_choices)) {
stop(paste("`spacing` must be one of",
paste0("'", spacing_choices, "'", collapse=", ")))
}
spacing_choices <- c("s", "xs", "m", "l")
if (!(spacing %in% spacing_choices)) {
stop(paste("`spacing` must be one of",
paste0("'", spacing_choices, "'", collapse=", ")))
}
# For css styling
classNames <- paste0("table shiny-table",
paste0(" table-", names(format)[format], collapse = "" ),
paste0(" spacing-", spacing))
# For css styling
classNames <- paste0("table shiny-table",
paste0(" table-", names(format)[format], collapse = "" ),
paste0(" spacing-", spacing))
data <- func()
data <- as.data.frame(data)
data <- as.data.frame(data)
# Return NULL if no data is provided
if (is.null(data) ||
(is.data.frame(data) && nrow(data) == 0 && ncol(data) == 0))
return(NULL)
# Return NULL if no data is provided
if (is.null(data) ||
(is.data.frame(data) && nrow(data) == 0 && ncol(data) == 0))
return(NULL)
# Separate the ... args to pass to xtable() vs print.xtable()
xtable_argnames <- setdiff(names(formals(xtable)), c("x", "..."))
xtable_args <- dots[intersect(names(dots), xtable_argnames)]
non_xtable_args <- dots[setdiff(names(dots), xtable_argnames)]
# Separate the ... args to pass to xtable() vs print.xtable()
xtable_argnames <- setdiff(names(formals(xtable)), c("x", "..."))
xtable_args <- dots[intersect(names(dots), xtable_argnames)]
non_xtable_args <- dots[setdiff(names(dots), xtable_argnames)]
# By default, numbers are right-aligned and everything else is left-aligned.
defaultAlignment <- function(col) {
if (is.numeric(col)) "r" else "l"
}
# By default, numbers are right-aligned and everything else is left-aligned.
defaultAlignment <- function(col) {
if (is.numeric(col)) "r" else "l"
}
# Figure out column alignment
## Case 1: default alignment
if (is.null(align) || align == "?") {
names <- defaultAlignment(attr(data, "row.names"))
cols <- paste(vapply(data, defaultAlignment, character(1)), collapse = "")
cols <- paste0(names, cols)
} else {
## Case 2: user-specified alignment
num_cols <- if (rownames) nchar(align) else nchar(align)+1
valid <- !grepl("[^lcr\\?]", align)
if (num_cols == ncol(data)+1 && valid) {
cols <- if (rownames) align else paste0("r", align)
defaults <- grep("\\?", strsplit(cols,"")[[1]])
if (length(defaults) != 0) {
vals <- vapply(data[,defaults-1], defaultAlignment, character(1))
for (i in seq_len(length(defaults))) {
substr(cols, defaults[i], defaults[i]) <- vals[i]
}
}
} else if (nchar(align) == 1 && valid) {
cols <- paste0(rep(align, ncol(data)+1), collapse="")
# Figure out column alignment
## Case 1: default alignment
if (is.null(align) || align == "?") {
names <- defaultAlignment(attr(data, "row.names"))
cols <- paste(vapply(data, defaultAlignment, character(1)), collapse = "")
cols <- paste0(names, cols)
} else {
stop("`align` must contain only the characters `l`, `c`, `r` and/or `?` and",
"have length either equal to 1 or to the total number of columns")
## Case 2: user-specified alignment
num_cols <- if (rownames) nchar(align) else nchar(align)+1
valid <- !grepl("[^lcr\\?]", align)
if (num_cols == ncol(data)+1 && valid) {
cols <- if (rownames) align else paste0("r", align)
defaults <- grep("\\?", strsplit(cols,"")[[1]])
if (length(defaults) != 0) {
vals <- vapply(data[,defaults-1], defaultAlignment, character(1))
for (i in seq_len(length(defaults))) {
substr(cols, defaults[i], defaults[i]) <- vals[i]
}
}
} else if (nchar(align) == 1 && valid) {
cols <- paste0(rep(align, ncol(data)+1), collapse="")
} else {
stop("`align` must contain only the characters `l`, `c`, `r` and/or `?` and",
"have length either equal to 1 or to the total number of columns")
}
}
}
# Call xtable with its (updated) args
xtable_args <- c(xtable_args, align = cols, digits = digits)
xtable_res <- do.call(xtable, c(list(data), xtable_args))
# Call xtable with its (updated) args
xtable_args <- c(xtable_args, align = cols, digits = digits)
xtable_res <- do.call(xtable, c(list(data), xtable_args))
# Set up print args
print_args <- list(
x = xtable_res,
type = 'html',
include.rownames = {
if ("include.rownames" %in% names(dots)) dots$include.rownames
else rownames
},
include.colnames = {
if ("include.colnames" %in% names(dots)) dots$include.colnames
else colnames
},
NA.string = {
if ("NA.string" %in% names(dots)) dots$NA.string
else na
},
html.table.attributes =
paste0({
if ("html.table.attributes" %in% names(dots)) dots$html.table.attributes
else ""
}, " ",
"class = '", htmlEscape(classNames, TRUE), "' ",
"style = 'width:", validateCssUnit(width), ";'"))
# Set up print args
print_args <- list(
x = xtable_res,
type = 'html',
include.rownames = {
if ("include.rownames" %in% names(dots)) dots$include.rownames
else rownames
},
include.colnames = {
if ("include.colnames" %in% names(dots)) dots$include.colnames
else colnames
},
NA.string = {
if ("NA.string" %in% names(dots)) dots$NA.string
else na
},
html.table.attributes =
paste0({
if ("html.table.attributes" %in% names(dots)) dots$html.table.attributes
else ""
}, " ",
"class = '", htmlEscape(classNames, TRUE), "' ",
"style = 'width:", validateCssUnit(width), ";'"),
comment = {
if ("comment" %in% names(dots)) dots$comment
else FALSE
}
)
print_args <- c(print_args, non_xtable_args)
print_args <- print_args[unique(names(print_args))]
print_args <- c(print_args, non_xtable_args)
print_args <- print_args[unique(names(print_args))]
# Capture the raw html table returned by print.xtable(), and store it in
# a variable for further processing
tab <- paste(utils::capture.output(do.call(print, print_args)),collapse = "\n")
# Capture the raw html table returned by print.xtable(), and store it in
# a variable for further processing
tab <- paste(utils::capture.output(do.call(print, print_args)),collapse = "\n")
# Add extra class to cells with NA value, to be able to style them separately
tab <- gsub(paste(">", na, "<"), paste(" class='NA'>", na, "<"), tab)
# Add extra class to cells with NA value, to be able to style them separately
tab <- gsub(paste(">", na, "<"), paste(" class='NA'>", na, "<"), tab)
# All further processing concerns the table headers, so we don't need to run
# any of this if colnames=FALSE
if (colnames) {
# Make sure that the final html table has a proper header (not included
# in the print.xtable() default)
tab <- sub("<tr>", "<thead> <tr>", tab)
tab <- sub("</tr>", "</tr> </thead> <tbody>", tab)
tab <- sub("</table>$", "</tbody> </table>", tab)
# All further processing concerns the table headers, so we don't need to run
# any of this if colnames=FALSE
if (colnames) {
# Make sure that the final html table has a proper header (not included
# in the print.xtable() default)
tab <- sub("<tr>", "<thead> <tr>", tab)
tab <- sub("</tr>", "</tr> </thead> <tbody>", tab)
tab <- sub("</table>$", "</tbody> </table>", tab)
# Update the `cols` string (which stores the alignment of each column) so
# that it only includes the alignment for the table variables (and not
# for the row.names)
cols <- if (rownames) cols else substr(cols, 2, nchar(cols))
# Update the `cols` string (which stores the alignment of each column) so
# that it only includes the alignment for the table variables (and not
# for the row.names)
cols <- if (rownames) cols else substr(cols, 2, nchar(cols))
# Create a vector whose i-th entry corresponds to the i-th table variable
# alignment (substituting "l" by "left", "c" by "center" and "r" by "right")
cols <- strsplit(cols, "")[[1]]
cols[cols == "l"] <- "left"
cols[cols == "r"] <- "right"
cols[cols == "c"] <- "center"
# Create a vector whose i-th entry corresponds to the i-th table variable
# alignment (substituting "l" by "left", "c" by "center" and "r" by "right")
cols <- strsplit(cols, "")[[1]]
cols[cols == "l"] <- "left"
cols[cols == "r"] <- "right"
cols[cols == "c"] <- "center"
# Align each header accordingly (this guarantees that each header and its
# corresponding column have the same alignment)
for (i in seq_len(length(cols))) {
tab <- sub("<th>", paste0("<th style='text-align: ", cols[i], ";'>"), tab)
# Align each header accordingly (this guarantees that each header and its
# corresponding column have the same alignment)
for (i in seq_len(length(cols))) {
tab <- sub("<th>", paste0("<th style='text-align: ", cols[i], ";'>"), tab)
}
}
}
return(tab)
}
# Main render function
markRenderFunction(tableOutput, renderFunc, outputArgs = outputArgs)
return(tab)
},
tableOutput, outputArgs
)
}

View File

@@ -1,3 +1,22 @@
#' Add a function for serializing an input before bookmarking application state
#'
#' @param inputId Name of the input value.
#' @param fun A function that takes the input value and returns a modified
#' value. The returned value will be used for the test snapshot.
#' @param session A Shiny session object.
#'
#' @keywords internal
#' @export
setSerializer <- function(inputId, fun, session = getDefaultReactiveDomain()) {
if (is.null(session)) {
stop("setSerializer() needs a session object.")
}
input_impl <- .subset2(session$input, "impl")
input_impl$setMeta(inputId, "shiny.serializer", fun)
}
# For most types of values, simply return the value unchanged.
serializerDefault <- function(value, stateDir) {
value
@@ -58,12 +77,12 @@ serializeReactiveValues <- function(values, exclude, stateDir = NULL) {
# Get the serializer function for this input value. If none specified, use
# the default.
serializer <- impl$getMeta(name, "shiny.serializer")
if (is.null(serializer))
serializer <- serializerDefault
serializer_fun <- impl$getMeta(name, "shiny.serializer")
if (is.null(serializer_fun))
serializer_fun <- serializerDefault
# Apply serializer function.
serializer(val, stateDir)
serializer_fun(val, stateDir)
})
# Filter out any values that were marked as unserializable.

View File

@@ -71,6 +71,63 @@ removeInputHandler <- function(type){
inputHandlers$remove(type)
}
# Apply input handler to a single input value
applyInputHandler <- function(name, val, shinysession) {
splitName <- strsplit(name, ':')[[1]]
if (length(splitName) > 1) {
if (!inputHandlers$containsKey(splitName[[2]])) {
# No input handler registered for this type
stop("No handler registered for type ", name)
}
inputName <- splitName[[1]]
# Get the function for processing this type of input
inputHandler <- inputHandlers$get(splitName[[2]])
return(inputHandler(val, shinysession, inputName))
} else if (is.list(val) && is.null(names(val))) {
return(unlist(val, recursive = TRUE))
} else {
return(val)
}
}
#' Apply input handlers to raw input values
#'
#' The purpose of this function is to make it possible for external packages to
#' test Shiny inputs. It takes a named list of raw input values, applies input
#' handlers to those values, and then returns a named list of the processed
#' values.
#'
#' The raw input values should be in a named list. Some values may have names
#' like \code{"x:shiny.date"}. This function would apply the \code{"shiny.date"}
#' input handler to the value, and then rename the result to \code{"x"}, in the
#' output.
#'
#' @param inputs A named list of input values.
#' @param shinysession A Shiny session object.
#'
#' @seealso registerInputHandler
#' @keywords internal
applyInputHandlers <- function(inputs, shinysession = getDefaultReactiveDomain()) {
inputs <- mapply(applyInputHandler, names(inputs), inputs,
MoreArgs = list(shinysession = shinysession),
SIMPLIFY = FALSE)
# Convert names like "button1:shiny.action" to "button1"
names(inputs) <- vapply(
names(inputs),
function(name) { strsplit(name, ":")[[1]][1] },
FUN.VALUE = character(1)
)
inputs
}
# Takes a list-of-lists and returns a matrix. The lists
# must all be the same length. NULL is replaced by NA.
registerInputHandler("shiny.matrix", function(data, ...) {
@@ -85,13 +142,14 @@ registerInputHandler("shiny.matrix", function(data, ...) {
return(m)
})
registerInputHandler("shiny.number", function(val, ...){
ifelse(is.null(val), NA, val)
})
registerInputHandler("shiny.password", function(val, shinysession, name) {
# Mark passwords as not serializable
.subset2(shinysession$input, "impl")$setMeta(name, "shiny.serializer", serializerUnserializable)
setSerializer(name, serializerUnserializable)
val
})
@@ -139,14 +197,45 @@ registerInputHandler("shiny.file", function(val, shinysession, name) {
# The data will be a named list of lists; convert to a data frame.
val <- as.data.frame(lapply(val, unlist), stringsAsFactors = FALSE)
# Make sure that the paths don't go up the directory tree, for security
# reasons.
if (any(grepl("..", val$datapath, fixed = TRUE))) {
stop("Invalid '..' found in file input path.")
# `val$datapath` should be a filename without a path, for security reasons.
if (basename(val$datapath) != val$datapath) {
stop("Invalid '/' found in file input path.")
}
# Prepend the persistent dir
val$datapath <- file.path(getCurrentRestoreContext()$dir, val$datapath)
oldfile <- file.path(getCurrentRestoreContext()$dir, val$datapath)
# Copy the original file to a new temp dir, so that a restored session can't
# modify the original.
newdir <- file.path(tempdir(), createUniqueId(12))
dir.create(newdir)
val$datapath <- file.path(newdir, val$datapath)
file.copy(oldfile, val$datapath)
# Need to mark this input value with the correct serializer. When a file is
# uploaded the usual way (instead of being restored), this occurs in
# session$`@uploadEnd`.
setSerializer(name, serializerFileInput)
snapshotPreprocessInput(name, snapshotPreprocessorFileInput)
val
})
# to be used with !!!answer
registerInputHandler("shiny.symbolList", function(val, ...) {
if (is.null(val)) {
list()
} else {
lapply(val, as.symbol)
}
})
# to be used with !!answer
registerInputHandler("shiny.symbol", function(val, ...) {
if (is.null(val) || identical(val, "")) {
NULL
} else {
as.symbol(val)
}
})

View File

@@ -1,6 +1,7 @@
#' @include server-input-handlers.R
appsByToken <- Map$new()
appsNeedingFlush <- Map$new()
# Provide a character representation of the WS that can be used
# as a key in a Map.
@@ -34,7 +35,7 @@ registerClient <- function(client) {
#' JavaScript/CSS files available to their components.
#'
#' @param prefix The URL prefix (without slashes). Valid characters are a-z,
#' A-Z, 0-9, hyphen, period, and underscore; and must begin with a-z or 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
@@ -52,21 +53,23 @@ registerClient <- function(client) {
#' @export
addResourcePath <- function(prefix, directoryPath) {
prefix <- prefix[1]
if (!grepl('^[a-z][a-z0-9\\-_.]*$', prefix, ignore.case=TRUE, perl=TRUE)) {
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")
}
directoryPath <- normalizePath(directoryPath, mustWork=TRUE)
existing <- .globals$resources[[prefix]]
.globals$resources[[prefix]] <- list(directoryPath=directoryPath,
func=staticHandler(directoryPath))
normalizedPath <- tryCatch(normalizePath(directoryPath, mustWork = TRUE),
error = function(e) {
stop("Couldn't normalize path in `addResourcePath`, with arguments: ",
"`prefix` = '", prefix, "'; `directoryPath` = '" , directoryPath, "'")
}
)
.globals$resources[[prefix]] <- list(
directoryPath = normalizedPath,
func = staticHandler(normalizedPath)
)
}
resourcePathHandler <- function(req) {
@@ -155,7 +158,7 @@ decodeMessage <- function(data) {
# Treat message as UTF-8
charData <- rawToChar(data)
Encoding(charData) <- 'UTF-8'
return(jsonlite::fromJSON(charData, simplifyVector=FALSE))
return(safeFromJSON(charData, simplifyVector=FALSE))
}
i <- 5
@@ -218,14 +221,15 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
if (is.character(msg))
msg <- charToRaw(msg)
if (isTRUE(getOption('shiny.trace'))) {
traceOption <- getOption('shiny.trace', FALSE)
if (isTRUE(traceOption) || traceOption == "recv") {
if (binary)
message("RECV ", '$$binary data$$')
else
message("RECV ", rawToChar(msg))
}
if (identical(charToRaw("\003\xe9"), msg))
if (isEmptyMessage(msg))
return()
msg <- decodeMessage(msg)
@@ -242,125 +246,87 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
} else {
# If there's bookmarked state, save it on the session object
shinysession$restoreContext <- RestoreContext$new(msg$data$.clientdata_url_search)
shinysession$createBookmarkObservers()
}
}
withRestoreContext(shinysession$restoreContext, {
unpackInput <- function(name, val) {
splitName <- strsplit(name, ':')[[1]]
if (length(splitName) > 1) {
if (!inputHandlers$containsKey(splitName[[2]])) {
# No input handler registered for this type
stop("No handler registered for type ", name)
msg$data <- applyInputHandlers(msg$data)
switch(
msg$method,
init = {
serverFunc <- withReactiveDomain(NULL, serverFuncSource())
if (!identicalFunctionBodies(serverFunc, appvars$server)) {
appvars$server <- serverFunc
if (!is.null(appvars$server))
{
# Tag this function as the Shiny server function. A debugger may use this
# tag to give this function special treatment.
# It's very important that it's appvars$server itself and NOT a copy that
# is invoked, otherwise new breakpoints won't be picked up.
attr(appvars$server, "shinyServerFunction") <- TRUE
registerDebugHook("server", appvars, "Server Function")
}
inputName <- splitName[[1]]
# Get the function for processing this type of input
inputHandler <- inputHandlers$get(splitName[[2]])
return(inputHandler(val, shinysession, inputName))
} else if (is.list(val) && is.null(names(val))) {
return(unlist(val, recursive = TRUE))
} else {
return(val)
}
}
msg$data <- mapply(unpackInput, names(msg$data), msg$data,
SIMPLIFY = FALSE)
# Check for switching into/out of showcase mode
if (.globals$showcaseOverride &&
exists(".clientdata_url_search", where = msg$data)) {
mode <- showcaseModeOfQuerystring(msg$data$.clientdata_url_search)
if (!is.null(mode))
shinysession$setShowcase(mode)
}
# Convert names like "button1:shiny.action" to "button1"
names(msg$data) <- vapply(
names(msg$data),
function(name) { strsplit(name, ":")[[1]][1] },
FUN.VALUE = character(1)
)
# In shinysession$createBookmarkObservers() above, observers may be
# created, which puts the shiny session in busyCount > 0 state. That
# prevents the manageInputs here from taking immediate effect, by
# default. The manageInputs here needs to take effect though, because
# otherwise the bookmark observers won't find the clientData they are
# looking for. So use `now = TRUE` to force the changes to be
# immediate.
#
# FIXME: break createBookmarkObservers into two separate steps, one
# before and one after manageInputs, and put the observer creation
# in the latter. Then add an assertion that busyCount == 0L when
# this manageInputs is called.
shinysession$manageInputs(msg$data, now = TRUE)
# The client tells us what singletons were rendered into
# the initial page
if (!is.null(msg$data$.clientdata_singletons)) {
shinysession$singletons <- strsplit(
msg$data$.clientdata_singletons, ',')[[1]]
}
switch(
msg$method,
init = {
local({
args <- argsForServerFunc(serverFunc, shinysession)
serverFunc <- withReactiveDomain(NULL, serverFuncSource())
if (!identicalFunctionBodies(serverFunc, appvars$server)) {
appvars$server <- serverFunc
if (!is.null(appvars$server))
{
# Tag this function as the Shiny server function. A debugger may use this
# tag to give this function special treatment.
# It's very important that it's appvars$server itself and NOT a copy that
# is invoked, otherwise new breakpoints won't be picked up.
attr(appvars$server, "shinyServerFunction") <- TRUE
registerDebugHook("server", appvars, "Server Function")
}
}
# Check for switching into/out of showcase mode
if (.globals$showcaseOverride &&
exists(".clientdata_url_search", where = msg$data)) {
mode <- showcaseModeOfQuerystring(msg$data$.clientdata_url_search)
if (!is.null(mode))
shinysession$setShowcase(mode)
}
shinysession$manageInputs(msg$data)
# The client tells us what singletons were rendered into
# the initial page
if (!is.null(msg$data$.clientdata_singletons)) {
shinysession$singletons <- strsplit(
msg$data$.clientdata_singletons, ',')[[1]]
}
local({
args <- argsForServerFunc(serverFunc, shinysession)
withReactiveDomain(shinysession, {
do.call(
# No corresponding ..stacktraceoff; the server func is pure
# user code
wrapFunctionLabel(appvars$server, "server",
..stacktraceon = TRUE
),
args
)
})
withReactiveDomain(shinysession, {
do.call(
# No corresponding ..stacktraceoff; the server func is pure
# user code
wrapFunctionLabel(appvars$server, "server",
..stacktraceon = TRUE
),
args
)
})
},
update = {
shinysession$manageInputs(msg$data)
},
shinysession$dispatch(msg)
)
shinysession$manageHiddenOutputs()
})
},
update = {
shinysession$manageInputs(msg$data)
},
shinysession$dispatch(msg)
)
# The HTTP_GUID, if it exists, is for Shiny Server reporting purposes
shinysession$startTiming(ws$request$HTTP_GUID)
shinysession$requestFlush()
if (exists(".shiny__stdout", globalenv()) &&
exists("HTTP_GUID", ws$request)) {
# safe to assume we're in shiny-server
shiny_stdout <- get(".shiny__stdout", globalenv())
# eNter a flushReact
writeLines(paste("_n_flushReact ", get("HTTP_GUID", ws$request),
" @ ", sprintf("%.3f", as.numeric(Sys.time())),
sep=""), con=shiny_stdout)
flush(shiny_stdout)
flushReact()
# eXit a flushReact
writeLines(paste("_x_flushReact ", get("HTTP_GUID", ws$request),
" @ ", sprintf("%.3f", as.numeric(Sys.time())),
sep=""), con=shiny_stdout)
flush(shiny_stdout)
} else {
flushReact()
}
flushAllSessions()
})
# Make httpuv return control to Shiny quickly, instead of waiting
# for the usual timeout
httpuv::interrupt()
})
}
ws$onMessage(function(binary, msg) {
@@ -371,6 +337,7 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
ws$onClose(function() {
shinysession$wsClosed()
appsByToken$remove(shinysession$token)
appsNeedingFlush$remove(shinysession$token)
})
return(TRUE)
@@ -400,9 +367,9 @@ argsForServerFunc <- function(serverFunc, session) {
}
getEffectiveBody <- function(func) {
# Note: NULL values are OK. isS4(NULL) returns FALSE, body(NULL)
# returns NULL.
if (isS4(func) && class(func) == "functionWithTrace")
if (is.null(func))
NULL
else if (isS4(func) && class(func) == "functionWithTrace")
body(func@original)
else
body(func)
@@ -452,7 +419,10 @@ startApp <- function(appObj, port, host, quiet) {
if (is.numeric(port) || is.integer(port)) {
if (!quiet) {
message('\n', 'Listening on http://', host, ':', port)
hostString <- host
if (httpuv::ipFamily(host) == 6L)
hostString <- paste0("[", hostString, "]")
message('\n', 'Listening on http://', hostString, ':', port)
}
return(startServer(host, port, handlerManager$createHttpuvApp()))
} else if (is.character(port)) {
@@ -473,25 +443,38 @@ startApp <- function(appObj, port, host, quiet) {
# Run an application that was created by \code{\link{startApp}}. This
# function should normally be called in a \code{while(TRUE)} loop.
serviceApp <- function() {
if (timerCallbacks$executeElapsed()) {
for (shinysession in appsByToken$values()) {
shinysession$manageHiddenOutputs()
}
timerCallbacks$executeElapsed()
flushReact()
flushAllSessions()
}
flushReact()
flushPendingSessions()
# If this R session is interactive, then call service() with a short timeout
# to keep the session responsive to user input
maxTimeout <- ifelse(interactive(), 100, 1000)
timeout <- max(1, min(maxTimeout, timerCallbacks$timeToNextEvent()))
timeout <- max(1, min(maxTimeout, timerCallbacks$timeToNextEvent(), later::next_op_secs()))
service(timeout)
flushReact()
flushPendingSessions()
}
.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.
#'
#' @return \code{TRUE} if a Shiny application is currently running. Otherwise,
#' \code{FALSE}.
#' @export
isRunning <- function() {
.globals$running
}
#' Run Shiny Application
#'
#' Runs a Shiny application. This function normally does not return; interrupt R
@@ -533,6 +516,9 @@ serviceApp <- function() {
#' application. If set to \code{"normal"}, displays the application normally.
#' Defaults to \code{"auto"}, which displays the application in the mode given
#' in its \code{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
#' \code{shiny.testmode} option, or FALSE if the option is not set.
#'
#' @examples
#' \dontrun{
@@ -545,6 +531,8 @@ serviceApp <- function() {
#'
#' ## 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(
@@ -576,25 +564,86 @@ runApp <- function(appDir=getwd(),
interactive()),
host=getOption('shiny.host', '127.0.0.1'),
workerId="", quiet=FALSE,
display.mode=c("auto", "normal", "showcase")) {
display.mode=c("auto", "normal", "showcase"),
test.mode=getOption('shiny.testmode', FALSE)) {
on.exit({
handlerManager$clear()
}, add = TRUE)
# Enable per-app Shiny options
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)
if (is.null(host) || is.na(host))
host <- '0.0.0.0'
# 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(warn = 1, pool.scheduler = scheduleTask)
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()) {
@@ -614,6 +663,11 @@ runApp <- function(appDir=getwd(),
# 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)) {
@@ -685,7 +739,8 @@ runApp <- function(appDir=getwd(),
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
if (!port %in% c(3659, 4045, 6000, 6665:6669)) {
# https://github.com/rstudio/shiny/issues/1784
if (!port %in% c(3659, 4045, 6000, 6665:6669, 6697)) {
break
}
}
@@ -701,17 +756,22 @@ runApp <- function(appDir=getwd(),
}
}
appParts <- as.shiny.appobj(appDir)
# 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 onEnd before we call onStart, so that it gets called even if an
# Set up the onStop before we call onStart, so that it gets called even if an
# error happens in onStart.
if (!is.null(appParts$onEnd))
on.exit(appParts$onEnd(), add = TRUE)
if (!is.null(appParts$onStop))
on.exit(appParts$onStop(), add = TRUE)
if (!is.null(appParts$onStart))
appParts$onStart()
@@ -722,8 +782,17 @@ runApp <- function(appDir=getwd(),
}, add = TRUE)
if (!is.character(port)) {
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
browseHost <- if (identical(host, "0.0.0.0")) "127.0.0.1" else host
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))
@@ -746,12 +815,12 @@ runApp <- function(appDir=getwd(),
# Top-level ..stacktraceoff..; matches with ..stacktraceon in observe(),
# reactive(), Callbacks$invoke(), and others
..stacktraceoff..(
captureStackTraces(
captureStackTraces({
while (!.globals$stopped) {
serviceApp()
..stacktracefloor..(serviceApp())
Sys.sleep(0.001)
}
)
})
)
if (isTRUE(.globals$reterror)) {
@@ -989,3 +1058,9 @@ browserViewer <- function(browser = getOption("browser")) {
inShinyServer <- function() {
nzchar(Sys.getenv('SHINY_PORT'))
}
# This check was moved out of the main function body because of an issue with
# the RStudio debugger. (#1474)
isEmptyMessage <- function(msg) {
identical(charToRaw("\003\xe9"), msg)
}

1884
R/shiny.R

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ NULL
#' # now we can just write "static" content without withMathJax()
#' div("more math here $$\\sqrt{2}$$")
withMathJax <- function(...) {
path <- 'https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'
path <- 'https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'
tagList(
tags$head(
singleton(tags$script(src = path, type = 'text/javascript'))
@@ -24,7 +24,7 @@ withMathJax <- function(...) {
)
}
renderPage <- function(ui, connection, showcase=0) {
renderPage <- function(ui, connection, showcase=0, testMode=FALSE) {
# 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")) {
@@ -45,11 +45,18 @@ renderPage <- function(ui, connection, showcase=0) {
shiny_deps <- list(
htmlDependency("json2", "2014.02.04", c(href="shared"), script = "json2-min.js"),
htmlDependency("jquery", "1.12.4", c(href="shared"), script = "jquery.min.js"),
htmlDependency("babel-polyfill", "6.7.2", c(href="shared"), script = "babel-polyfill.min.js"),
htmlDependency("shiny", utils::packageVersion("shiny"), c(href="shared"),
script = if (getOption("shiny.minified", TRUE)) "shiny.min.js" else "shiny.js",
stylesheet = "shiny.css")
)
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")
}
html <- renderDocument(ui, shiny_deps, processDep = createWebDependency)
writeUTF8(html, con = connection)
}
@@ -91,6 +98,8 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
showcaseMode <- mode
}
testMode <- .globals$testMode %OR% FALSE
# Create a restore context using query string
bookmarkStore <- getShinyOption("bookmarkStore", default = "disable")
if (bookmarkStore == "disable") {
@@ -121,7 +130,7 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
if (is.null(uiValue))
return(NULL)
renderPage(uiValue, textConn, showcaseMode)
renderPage(uiValue, textConn, showcaseMode, testMode)
html <- paste(readLines(textConn, encoding = 'UTF-8'), collapse='\n')
return(httpResponse(200, content=enc2utf8(html)))
}

View File

@@ -1,4 +1,4 @@
globalVariables('func')
utils::globalVariables('func')
#' Mark a function as a render function
#'
@@ -52,6 +52,49 @@ markRenderFunction <- function(uiFunc, renderFunc, outputArgs = list()) {
hasExecuted = hasExecuted)
}
#' Implement render functions
#'
#' @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.
#' @param transform A function that takes four arguments: \code{value},
#' \code{session}, \code{name}, and \code{...} (for future-proofing). This
#' function will be invoked each time a value is returned from \code{func},
#' and is responsible for changing the value into a JSON-ready value to be
#' JSON-encoded and sent to the browser.
#' @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 \code{outputFunc}.
#' Render functions should include \code{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).
#' @return An annotated render function, ready to be assigned to an
#' \code{output} slot.
#'
#' @export
createRenderFunction <- function(
func, transform = function(value, session, name, ...) value,
outputFunc = NULL, outputArgs = NULL
) {
renderFunc <- function(shinysession, name, ...) {
hybrid_chain(
func(),
function(value, .visible) {
transform(setVisible(value, .visible), shinysession, name, ...)
}
)
}
if (!is.null(outputFunc))
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
else
renderFunc
}
useRenderFunction <- function(renderFunc, inline = FALSE) {
outputFunction <- attr(renderFunc, "outputFunc")
outputArgs <- attr(renderFunc, "outputArgs")
@@ -68,12 +111,16 @@ useRenderFunction <- function(renderFunc, inline = FALSE) {
}
id <- createUniqueId(8, "out")
# Make the id the first positional argument
outputArgs <- c(list(id), outputArgs)
o <- getDefaultReactiveDomain()$output
if (!is.null(o))
if (!is.null(o)) {
o[[id]] <- renderFunc
# If there's a namespace, we must respect it
id <- getDefaultReactiveDomain()$ns(id)
}
# Make the id the first positional argument
outputArgs <- c(list(id), outputArgs)
if (is.logical(formals(outputFunction)[["inline"]]) && !("inline" %in% names(outputArgs))) {
outputArgs[["inline"]] <- inline
@@ -88,6 +135,34 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
useRenderFunction(x, inline = inline)
}
#' Mark a render function with attributes that will be used by the output
#'
#' @inheritParams markRenderFunction
#' @param snapshotExclude If TRUE, exclude the output from test snapshots.
#' @param snapshotPreprocess A function for preprocessing the value before
#' taking a test snapshot.
#'
#' @keywords internal
markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
snapshotPreprocess = NULL)
{
# Add the outputAttrs attribute if necessary
if (is.null(attr(renderFunc, "outputAttrs", TRUE))) {
attr(renderFunc, "outputAttrs") <- list()
}
if (!is.null(snapshotExclude)) {
attr(renderFunc, "outputAttrs")$snapshotExclude <- snapshotExclude
}
if (!is.null(snapshotPreprocess)) {
attr(renderFunc, "outputAttrs")$snapshotPreprocess <- snapshotPreprocess
}
renderFunc
}
#' Image file output
#'
#' Renders a reactive image that is suitable for assigning to an \code{output}
@@ -127,6 +202,7 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
#' @examples
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
#' options(device.ask.default = FALSE)
#'
#' ui <- fluidPage(
#' sliderInput("n", "Number of observations", 2, 1000, 500),
@@ -193,26 +269,25 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
deleteFile=TRUE, outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
renderFunc <- function(shinysession, name, ...) {
imageinfo <- func()
# 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))
}
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))
}
# If contentType not specified, autodetect based on extension
contentType <- imageinfo$contentType %OR% getContentType(imageinfo$src)
# If contentType not specified, autodetect based on extension
contentType <- imageinfo$contentType %OR% getContentType(imageinfo$src)
# Extra values are everything in imageinfo except 'src' and 'contentType'
extra_attr <- imageinfo[!names(imageinfo) %in% c('src', 'contentType')]
# Extra values are everything in imageinfo except 'src' and 'contentType'
extra_attr <- imageinfo[!names(imageinfo) %in% c('src', 'contentType')]
# Return a list with src, and other img attributes
c(src = shinysession$fileUrl(name, file=imageinfo$src, contentType=contentType),
extra_attr)
}
markRenderFunction(imageOutput, renderFunc, outputArgs = outputArgs)
# Return a list with src, and other img attributes
c(src = session$fileUrl(name, file=imageinfo$src, contentType=contentType),
extra_attr)
},
imageOutput, outputArgs)
}
@@ -220,7 +295,7 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
#'
#' Makes a reactive version of the given function that captures any printed
#' output, and also captures its printable result (unless
#' \code{\link{invisible}}), into a string. The resulting function is suitable
#' \code{\link[base]{invisible}}), into a string. The resulting function is suitable
#' for assigning to an \code{output} slot.
#'
#' The corresponding HTML output tag can be anything (though \code{pre} is
@@ -232,14 +307,14 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
#'
#' Note that unlike most other Shiny output functions, if the given function
#' returns \code{NULL} then \code{NULL} will actually be visible in the output.
#' To display nothing, make your function return \code{\link{invisible}()}.
#' To display nothing, make your function return \code{\link[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 \code{expr}.
#' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This
#' is useful if you want to save an expression in a variable.
#' @param width The value for \code{\link{options}('width')}.
#' @param width The value for \code{\link[base]{options}('width')}.
#' @param outputArgs A list of arguments to be passed through to the implicit
#' call to \code{\link{verbatimTextOutput}} when \code{renderPrint} is used
#' in an interactive R Markdown document.
@@ -252,15 +327,74 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
width = getOption('width'), outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
# Set a promise domain that sets the console width
# and captures output
# op <- options(width = width)
# on.exit(options(op), add = TRUE)
renderFunc <- function(shinysession, name, ...) {
op <- options(width = width)
on.exit(options(op), add = TRUE)
paste(utils::capture.output(func()), collapse = "\n")
domain <- createRenderPrintPromiseDomain(width)
hybrid_chain(
{
promises::with_promise_domain(domain, func())
},
function(value, .visible) {
if (.visible) {
cat(file = domain$conn, paste(utils::capture.output(value, append = TRUE), collapse = "\n"))
}
res <- paste(readLines(domain$conn, warn = FALSE), collapse = "\n")
res
},
finally = function() {
close(domain$conn)
}
)
}
markRenderFunction(verbatimTextOutput, renderFunc, outputArgs = outputArgs)
}
createRenderPrintPromiseDomain <- function(width) {
f <- file()
promises::new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
function(...) {
op <- options(width = width)
on.exit(options(op), add = TRUE)
sink(f, append = TRUE)
on.exit(sink(NULL), add = TRUE)
onFulfilled(...)
}
},
wrapOnRejected = function(onRejected) {
force(onRejected)
function(...) {
op <- options(width = width)
on.exit(options(op), add = TRUE)
sink(f, append = TRUE)
on.exit(sink(NULL), add = TRUE)
onRejected(...)
}
},
wrapSync = function(expr) {
op <- options(width = width)
on.exit(options(op), add = TRUE)
sink(f, append = TRUE)
on.exit(sink(NULL), add = TRUE)
force(expr)
},
conn = f
)
}
#' Text Output
#'
#' Makes a reactive version of the given function that also uses
@@ -292,18 +426,18 @@ renderText <- function(expr, env=parent.frame(), quoted=FALSE,
outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
renderFunc <- function(shinysession, name, ...) {
value <- func()
return(paste(utils::capture.output(cat(value)), collapse="\n"))
}
markRenderFunction(textOutput, renderFunc, outputArgs = outputArgs)
createRenderFunction(
func,
function(value, session, name, ...) {
paste(utils::capture.output(cat(value)), collapse="\n")
},
textOutput, outputArgs
)
}
#' UI Output
#'
#' \bold{Experimental feature.} Makes a reactive version of a function that
#' generates HTML using the Shiny UI library.
#' Renders reactive HTML using the Shiny UI library.
#'
#' The corresponding HTML output tag should be \code{div} and have the CSS class
#' name \code{shiny-html-output} (or use \code{\link{uiOutput}}).
@@ -317,7 +451,7 @@ renderText <- function(expr, env=parent.frame(), quoted=FALSE,
#' call to \code{\link{uiOutput}} when \code{renderUI} is used in an
#' interactive R Markdown document.
#'
#' @seealso conditionalPanel
#' @seealso \code{\link{uiOutput}}
#' @export
#' @examples
#' ## Only run examples in interactive R sessions
@@ -342,15 +476,16 @@ renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
renderFunc <- function(shinysession, name, ...) {
result <- func()
if (is.null(result) || length(result) == 0)
return(NULL)
createRenderFunction(
func,
function(result, shinysession, name, ...) {
if (is.null(result) || length(result) == 0)
return(NULL)
processDeps(result, shinysession)
}
markRenderFunction(uiOutput, renderFunc, outputArgs = outputArgs)
processDeps(result, shinysession)
},
uiOutput, outputArgs
)
}
#' File Downloads
@@ -409,7 +544,9 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
renderFunc <- function(shinysession, name, ...) {
shinysession$registerDownload(name, filename, contentType, content)
}
markRenderFunction(downloadButton, renderFunc, outputArgs = outputArgs)
snapshotExclude(
markRenderFunction(downloadButton, renderFunc, outputArgs = outputArgs)
)
}
#' Table output with the JavaScript library DataTables
@@ -420,7 +557,7 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
#' the server infrastructure.
#'
#' For the \code{options} argument, the character elements that have the class
#' \code{"AsIs"} (usually returned from \code{\link{I}()}) will be evaluated in
#' \code{"AsIs"} (usually returned from \code{\link[base]{I}()}) will be evaluated in
#' JavaScript. This is useful when the type of the option value is not supported
#' in JSON, e.g., a JavaScript function, which can be obtained by evaluating a
#' character string. Note this only applies to the root-level elements of the
@@ -486,31 +623,46 @@ renderDataTable <- function(expr, options = NULL, searchDelay = 500,
if (is.function(options)) options <- options()
options <- checkDT9(options)
res <- checkAsIs(options)
data <- func()
if (length(dim(data)) != 2) return() # expects a rectangular data object
if (is.data.frame(data)) data <- as.data.frame(data)
action <- shinysession$registerDataObj(name, data, dataTablesJSON)
colnames <- colnames(data)
# if escape is column names, turn names to numeric indices
if (is.character(escape)) {
escape <- stats::setNames(seq_len(ncol(data)), colnames)[escape]
if (any(is.na(escape)))
stop("Some column names in the 'escape' argument not found in data")
}
colnames[escape] <- htmlEscape(colnames[escape])
if (!is.logical(escape)) {
if (!is.numeric(escape))
stop("'escape' must be TRUE, FALSE, or a numeric vector, or column names")
escape <- paste(escape, collapse = ',')
}
list(
colnames = colnames, action = action, options = res$options,
evalOptions = if (length(res$eval)) I(res$eval), searchDelay = searchDelay,
callback = paste(callback, collapse = '\n'), escape = escape
hybrid_chain(
func(),
function(data) {
if (length(dim(data)) != 2) return() # expects a rectangular data object
if (is.data.frame(data)) data <- as.data.frame(data)
action <- shinysession$registerDataObj(name, data, dataTablesJSON)
colnames <- colnames(data)
# if escape is column names, turn names to numeric indices
if (is.character(escape)) {
escape <- stats::setNames(seq_len(ncol(data)), colnames)[escape]
if (any(is.na(escape)))
stop("Some column names in the 'escape' argument not found in data")
}
colnames[escape] <- htmlEscape(colnames[escape])
if (!is.logical(escape)) {
if (!is.numeric(escape))
stop("'escape' must be TRUE, FALSE, or a numeric vector, or column names")
escape <- paste(escape, collapse = ',')
}
list(
colnames = colnames, action = action, options = res$options,
evalOptions = if (length(res$eval)) I(res$eval), searchDelay = searchDelay,
callback = paste(callback, collapse = '\n'), escape = escape
)
}
)
}
markRenderFunction(dataTableOutput, renderFunc, outputArgs = outputArgs)
renderFunc <- markRenderFunction(dataTableOutput, renderFunc, outputArgs = outputArgs)
renderFunc <- snapshotPreprocessOutput(renderFunc, function(value) {
# Remove the action field so that it's not saved in test snapshots. It
# contains a value that changes every time an app is run, and shouldn't be
# stored for test snapshots. It will be something like:
# "session/e0d14d3fe97f672f9655a127f2a1e079/dataobj/table?w=&nonce=7f5d6d54e22450a3"
value$action <- NULL
value
})
renderFunc
}
# a data frame containing the DataTables 1.9 and 1.10 names

View File

@@ -18,7 +18,8 @@ licenseLink <- function(licenseName) {
"Artistic-2.0" = "http://www.r-project.org/Licenses/Artistic-2.0",
"BSD_2_clause" = "http://www.r-project.org/Licenses/BSD_2_clause",
"BSD_3_clause" = "http://www.r-project.org/Licenses/BSD_3_clause",
"MIT" = "http://www.r-project.org/Licenses/MIT")
"MIT" = "http://www.r-project.org/Licenses/MIT",
"CC-BY-SA-4.0" = "https://www.r-project.org/Licenses/CC-BY-SA-4.0")
if (exists(licenseName, where = licenses)) {
tags$a(href=licenses[[licenseName]], licenseName)
} else {
@@ -31,7 +32,7 @@ licenseLink <- function(licenseName) {
showcaseHead <- function() {
deps <- list(
htmlDependency("jqueryui", "1.11.4", c(href="shared/jqueryui"),
htmlDependency("jqueryui", "1.12.1", c(href="shared/jqueryui"),
script = "jquery-ui.min.js"),
htmlDependency("showdown", "0.3.1", c(href="shared/showdown/compressed"),
script = "showdown.js"),

44
R/snapshot.R Normal file
View File

@@ -0,0 +1,44 @@
#' Mark an output to be excluded from test snapshots
#'
#' @param x A reactive which will be assigned to an output.
#'
#' @export
snapshotExclude <- function(x) {
markOutputAttrs(x, snapshotExclude = TRUE)
}
#' Add a function for preprocessing an output before taking a test snapshot
#'
#' @param x A reactive which will be assigned to an output.
#' @param fun A function that takes the output value as an input and returns a
#' modified value. The returned value will be used for the test snapshot.
#'
#' @export
snapshotPreprocessOutput <- function(x, fun) {
markOutputAttrs(x, snapshotPreprocess = fun)
}
#' Add a function for preprocessing an input before taking a test snapshot
#'
#' @param inputId Name of the input value.
#' @param fun A function that takes the input value and returns a modified
#' value. The returned value will be used for the test snapshot.
#' @param session A Shiny session object.
#'
#' @export
snapshotPreprocessInput <- function(inputId, fun, session = getDefaultReactiveDomain()) {
if (is.null(session)) {
stop("snapshotPreprocessInput() needs a session object.")
}
input_impl <- .subset2(session$input, "impl")
input_impl$setMeta(inputId, "shiny.snapshot.preprocess", fun)
}
# Strip out file path from fileInput value
snapshotPreprocessorFileInput <- function(value) {
value$datapath <- basename(value$datapath)
value
}

61
R/test-export.R Normal file
View File

@@ -0,0 +1,61 @@
#' Register expressions for export in test mode
#'
#' This function registers expressions that will be evaluated when a test export
#' event occurs. These events are triggered by accessing a snapshot URL.
#'
#' This function only has an effect if the app is launched in test mode. This is
#' done by calling \code{runApp()} with \code{test.mode=TRUE}, or by setting the
#' global option \code{shiny.testmode} to \code{TRUE}.
#'
#' @param quoted_ Are the expression quoted? Default is \code{FALSE}.
#' @param env_ The environment in which the expression should be evaluated.
#' @param session_ A Shiny session object.
#' @param ... Named arguments that are quoted or unquoted expressions that will
#' be captured and evaluated when snapshot URL is visited.
#' @examples
#' ## Only run this example in interactive R sessions
#' if (interactive()) {
#'
#' options(shiny.testmode = TRUE)
#'
#' # This application shows the test snapshot URL; clicking on it will
#' # fetch the input, output, and exported values in JSON format.
#' shinyApp(
#' ui = basicPage(
#' h4("Snapshot URL: "),
#' uiOutput("url"),
#' h4("Current values:"),
#' verbatimTextOutput("values"),
#' actionButton("inc", "Increment x")
#' ),
#'
#' server = function(input, output, session) {
#' vals <- reactiveValues(x = 1)
#' y <- reactive({ vals$x + 1 })
#'
#' observeEvent(input$inc, {
#' vals$x <<- vals$x + 1
#' })
#'
#' exportTestValues(
#' x = vals$x,
#' y = y()
#' )
#'
#' output$url <- renderUI({
#' url <- session$getTestSnapshotUrl(format="json")
#' a(href = url, url)
#' })
#'
#' output$values <- renderText({
#' paste0("vals$x: ", vals$x, "\ny: ", y())
#' })
#' }
#' )
#' }
#' @export
exportTestValues <- function(..., quoted_ = FALSE, env_ = parent.frame(),
session_ = getDefaultReactiveDomain())
{
session_$exportTestValues(..., quoted_ = quoted_, env_ = env_)
}

View File

@@ -42,6 +42,17 @@ TimerCallbacks <- R6Class(
return(id)
},
unschedule = function(id) {
toRemoveIndices <- .times$id %in% id
toRemoveIds <- .times[toRemoveIndices, "id", drop = TRUE]
if (length(toRemoveIds) > 0) {
.times <<- .times[!toRemoveIndices,]
for (toRemoveId in as.character(toRemoveIds)) {
.funcs$remove(toRemoveId)
}
}
return(id %in% toRemoveIds)
},
timeToNextEvent = function() {
if (dim(.times)[1] == 0)
return(Inf)
@@ -79,13 +90,9 @@ timerCallbacks <- TimerCallbacks$new()
scheduleTask <- function(millis, callback) {
cancelled <- FALSE
timerCallbacks$schedule(millis, function() {
if (!cancelled)
callback()
})
id <- timerCallbacks$schedule(millis, callback)
function() {
cancelled <<- TRUE
callback <<- NULL # to allow for callback to be gc'ed
invisible(timerCallbacks$unschedule(id))
}
}

View File

@@ -2,6 +2,7 @@
#'
#' @template update-input
#' @param value The value to set for the input object.
#' @param placeholder The placeholder to set for the input object.
#'
#' @seealso \code{\link{textInput}}
#'
@@ -34,15 +35,15 @@
#' shinyApp(ui, server)
#' }
#' @export
updateTextInput <- function(session, inputId, label = NULL, value = NULL) {
message <- dropNulls(list(label=label, value=value))
updateTextInput <- function(session, inputId, label = NULL, value = NULL, placeholder = NULL) {
message <- dropNulls(list(label=label, value=value, placeholder=placeholder))
session$sendInputMessage(inputId, message)
}
#' Change the value of a textarea input on the client
#'
#' @template update-input
#' @param value The value to set for the input object.
#' @inheritParams updateTextInput
#'
#' @seealso \code{\link{textAreaInput}}
#'
@@ -106,7 +107,10 @@ updateTextAreaInput <- updateTextInput
#' shinyApp(ui, server)
#' }
#' @export
updateCheckboxInput <- updateTextInput
updateCheckboxInput <- function(session, inputId, label = NULL, value = NULL) {
message <- dropNulls(list(label=label, value=value))
session$sendInputMessage(inputId, message)
}
#' Change the label or icon of an action button on the client
@@ -179,21 +183,18 @@ updateActionButton <- function(session, inputId, label = NULL, icon = NULL) {
#' if (interactive()) {
#'
#' ui <- fluidPage(
#' sliderInput("controller", "Controller", 1, 30, 10),
#' sliderInput("n", "Day of month", 1, 30, 10),
#' dateInput("inDate", "Input date")
#' )
#'
#' server <- function(input, output, session) {
#' observe({
#' # We'll use the input$controller variable multiple times, so save it as x
#' # for convenience.
#' x <- input$controller
#'
#' date <- as.Date(paste0("2013-04-", input$n))
#' updateDateInput(session, "inDate",
#' label = paste("Date label", x),
#' value = paste("2013-04-", x, sep=""),
#' min = paste("2013-04-", x-1, sep=""),
#' max = paste("2013-04-", x+1, sep="")
#' label = paste("Date label", input$n),
#' value = date,
#' min = date - 3,
#' max = date + 3
#' )
#' })
#' }
@@ -241,20 +242,20 @@ updateDateInput <- function(session, inputId, label = NULL, value = NULL,
#' if (interactive()) {
#'
#' ui <- fluidPage(
#' sliderInput("controller", "Controller", 1, 30, 10),
#' sliderInput("n", "Day of month", 1, 30, 10),
#' dateRangeInput("inDateRange", "Input date range")
#' )
#'
#' server <- function(input, output, session) {
#' observe({
#' # We'll use the input$controller variable multiple times, so save it as x
#' # for convenience.
#' x <- input$controller
#' date <- as.Date(paste0("2013-04-", input$n))
#'
#' updateDateRangeInput(session, "inDateRange",
#' label = paste("Date range label", x),
#' start = paste("2013-01-", x, sep=""),
#' end = paste("2013-12-", x, sep="")
#' label = paste("Date range label", input$n),
#' start = date - 1,
#' end = date + 1,
#' min = date - 5,
#' max = date + 5
#' )
#' })
#' }
@@ -382,13 +383,17 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
session$sendInputMessage(inputId, message)
}
#' Change the value of a slider input on the client
#' Update Slider Input Widget
#'
#' 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.
#'
#' @seealso \code{\link{sliderInput}}
#'
@@ -421,22 +426,15 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
#' }
#' @export
updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
min = NULL, max = NULL, step = NULL)
min = NULL, max = NULL, step = NULL, timeFormat = NULL, timezone = NULL)
{
# Make sure that value, min, max all have the same type, because we need
# special handling for dates and datetimes.
vals <- dropNulls(list(value, min, max))
dataType <- getSliderType(min, max, value)
type <- unique(lapply(vals, function(x) {
if (inherits(x, "Date")) "date"
else if (inherits(x, "POSIXt")) "datetime"
else "number"
}))
if (length(type) > 1) {
stop("Type mismatch for value, min, and max")
if (is.null(timeFormat)) {
timeFormat <- switch(dataType, date = "%F", datetime = "%F %T", number = NULL)
}
if ((length(type) == 1) && (type == "date" || type == "datetime")) {
if (dataType == "date" || dataType == "datetime") {
to_ms <- function(x) 1000 * as.numeric(as.POSIXct(x))
if (!is.null(min)) min <- to_ms(min)
if (!is.null(max)) max <- to_ms(max)
@@ -448,23 +446,28 @@ updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
value = formatNoSci(value),
min = formatNoSci(min),
max = formatNoSci(max),
step = formatNoSci(step)
step = formatNoSci(step),
`data-type` = dataType,
`time-format` = timeFormat,
timezone = timezone
))
session$sendInputMessage(inputId, message)
}
updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
selected = NULL, inline = FALSE,
type = 'checkbox') {
if (!is.null(choices))
choices <- choicesWithNames(choices)
if (!is.null(selected))
selected <- validateSelected(selected, choices, session$ns(inputId))
selected = NULL, inline = FALSE, type = NULL,
choiceNames = NULL, choiceValues = NULL) {
if (is.null(type)) stop("Please specify the type ('checkbox' or 'radio')")
options <- if (!is.null(choices)) {
args <- normalizeChoicesArgs(choices, choiceNames, choiceValues, mustExist = FALSE)
if (!is.null(selected)) selected <- as.character(selected)
options <- if (!is.null(args$choiceValues)) {
format(tagList(
generateOptions(session$ns(inputId), choices, selected, inline, type = type)
generateOptions(session$ns(inputId), selected, inline, type,
args$choiceNames, args$choiceValues)
))
}
@@ -513,9 +516,10 @@ updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
#' }
#' @export
updateCheckboxGroupInput <- function(session, inputId, label = NULL,
choices = NULL, selected = NULL,
inline = FALSE) {
updateInputOptions(session, inputId, label, choices, selected, inline)
choices = NULL, selected = NULL, inline = FALSE,
choiceNames = NULL, choiceValues = NULL) {
updateInputOptions(session, inputId, label, choices, selected,
inline, "checkbox", choiceNames, choiceValues)
}
@@ -555,13 +559,15 @@ updateCheckboxGroupInput <- function(session, inputId, label = NULL,
#' }
#' @export
updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
selected = NULL, inline = FALSE) {
if (!is.null(choices)) choices <- as.character(choices)
if (!is.null(selected)) selected <- as.character(selected)
selected = NULL, inline = FALSE,
choiceNames = NULL, choiceValues = NULL) {
# you must select at least one radio button
if (is.null(selected) && !is.null(choices)) selected <- choices[[1]]
updateInputOptions(session, inputId, label, choices, selected, inline, type = 'radio')
if (is.null(selected)) {
if (!is.null(choices)) selected <- choices[[1]]
else if (!is.null(choiceValues)) selected <- choiceValues[[1]]
}
updateInputOptions(session, inputId, label, choices, selected,
inline, 'radio', choiceNames, choiceValues)
}
@@ -570,7 +576,7 @@ updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
#' @template update-input
#' @inheritParams selectInput
#'
#' @seealso \code{\link{selectInput}}
#' @seealso \code{\link{selectInput}} \code{\link{varSelectInput}}
#'
#' @examples
#' ## Only run examples in interactive R sessions
@@ -607,8 +613,7 @@ updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
updateSelectInput <- function(session, inputId, label = NULL, choices = NULL,
selected = NULL) {
choices <- if (!is.null(choices)) choicesWithNames(choices)
if (!is.null(selected))
selected <- validateSelected(selected, choices, inputId)
if (!is.null(selected)) selected <- as.character(selected)
options <- if (!is.null(choices)) selectOptions(choices, selected)
message <- dropNulls(list(label = label, options = options, value = selected))
session$sendInputMessage(inputId, message)
@@ -628,7 +633,7 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
res <- checkAsIs(options)
cfg <- tags$script(
type = 'application/json',
`data-for` = inputId,
`data-for` = session$ns(inputId),
`data-eval` = if (length(res$eval)) HTML(toJSON(res$eval)),
HTML(toJSON(res$options))
)
@@ -637,8 +642,95 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
if (!server) {
return(updateSelectInput(session, inputId, label, choices, selected))
}
noOptGroup <- TRUE
if (is.list(choices)) {
# check if list is nested
for (i in seq_along(choices)) {
if (is.list(choices[[i]]) || length(choices[[i]]) > 1) {
noOptGroup <- FALSE
break()
}
}
}
# convert choices to a data frame so it returns [{label: , value: , optgroup: },...]
choices <- if (is.data.frame(choices)) {
# jcheng 2018/09/25: I don't think we ever said data frames were OK to pass
# to updateSelectInput, but one of the example apps does this and at least
# one user noticed when we broke it.
# https://github.com/rstudio/shiny/issues/2172
# https://github.com/rstudio/shiny/issues/2192
as.data.frame(choices, stringsAsFactors = FALSE)
} else if (is.atomic(choices) || noOptGroup) {
# fast path for vectors and flat lists
if (is.list(choices)) {
choices <- unlist(choices)
}
if (is.null(names(choices))) {
lab <- as.character(choices)
} else {
lab <- names(choices)
# replace empty names like: choices = c(a = 1, 2)
# in this case: names(choices) = c("a", "")
# with replacement below choices will be: lab = c("a", "2")
empty_names_indices <- lab == ""
lab[empty_names_indices] <- as.character(choices[empty_names_indices])
}
data.frame(label = lab, value = choices, stringsAsFactors = FALSE)
} else {
# slow path for nested lists/optgroups
list_names <- names(choices)
if (is.null(list_names)) {
list_names <- rep("", length(choices))
}
choice_list <- mapply(choices, list_names, FUN = function (choice, name) {
group <- ""
lab <- name
if (lab == "") lab <- as.character(choice)
if (is.list(choice) || length(choice) > 1) {
group <- rep(name, length(choice))
choice <- unlist(choice)
if (is.null(names(choice))) {
lab <- as.character(choice)
} else {
lab <- names(choice)
# replace empty names like: choices = c(a = 1, 2)
# in this case: names(choices) = c("a", "")
# with replacement below choices will be: lab = c("a", "2")
empty_names_indices <- lab == ""
lab[empty_names_indices] <- as.character(choice[empty_names_indices])
}
}
list(
label = lab,
value = as.character(choice),
# The name "optgroup" is because this is the default field where
# selectize will look for group IDs
optgroup = group
)
}, SIMPLIFY = FALSE)
extract_vector <- function(x, name) {
vecs <- lapply(x, `[[`, name)
do.call(c, vecs)
}
data.frame(
label = extract_vector(choice_list, "label"),
value = extract_vector(choice_list, "value"),
optgroup = extract_vector(choice_list, "optgroup"),
stringsAsFactors = FALSE, row.names = NULL
)
}
value <- unname(selected)
attr(choices, 'selected_value') <- value
message <- dropNulls(list(
label = label,
value = value,
@@ -646,38 +738,76 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
))
session$sendInputMessage(inputId, message)
}
#' @rdname updateSelectInput
#' @inheritParams varSelectInput
#' @export
updateVarSelectInput <- function(session, inputId, label = NULL, data = NULL, selected = NULL) {
if (is.null(data)) {
choices <- NULL
} else {
choices <- colnames(data)
}
updateSelectInput(
session = session,
inputId = inputId,
label = label,
choices = choices,
selected = selected
)
}
#' @rdname updateSelectInput
#' @export
updateVarSelectizeInput <- function(session, inputId, label = NULL, data = NULL, selected = NULL, options = list(), server = FALSE) {
if (is.null(data)) {
choices <- NULL
} else {
choices <- colnames(data)
}
updateSelectizeInput(
session = session,
inputId = inputId,
label = label,
choices = choices,
selected = selected,
options = options,
server = server
)
}
selectizeJSON <- function(data, req) {
query <- parseQueryString(req$QUERY_STRING)
# extract the query variables, conjunction (and/or), search string, maximum options
var <- c(jsonlite::fromJSON(query$field))
cjn <- if (query$conju == 'and') all else any
var <- c(safeFromJSON(query$field))
# all keywords in lower-case, for case-insensitive matching
key <- unique(strsplit(tolower(query$query), '\\s+')[[1]])
if (identical(key, '')) key <- character(0)
mop <- as.numeric(query$maxop)
vfd <- query$value # the value field name
sel <- attr(data, 'selected_value', exact = TRUE)
# convert a single vector to a data frame so it returns {label: , value: }
# later in JSON; other objects return arbitrary JSON {x: , y: , foo: , ...}
data <- if (is.atomic(data)) {
data.frame(label = names(choicesWithNames(data)), value = data,
stringsAsFactors = FALSE)
} else as.data.frame(data, stringsAsFactors = FALSE)
# start searching for keywords in all specified columns
idx <- logical(nrow(data))
if (length(key)) for (v in var) {
matches <- do.call(
cbind,
lapply(key, function(k) {
grepl(k, tolower(as.character(data[[v]])), fixed = TRUE)
})
)
# merge column matches using OR, and match multiple keywords in one column
# using the conjunction setting (AND or OR)
idx <- idx | apply(matches, 1, cjn)
if (length(key)) {
for (v in var) {
matches <- do.call(
cbind,
lapply(key, function(k) {
grepl(k, tolower(as.character(data[[v]])), fixed = TRUE)
})
)
# merge column matches using OR, and match multiple keywords in one column
# using the conjunction setting (AND or OR)
matches <- rowSums(matches)
if (query$conju == 'and')
idx <- idx | (matches == length(key))
else
idx <- idx | matches
}
}
# only return the first n rows (n = maximum options in configuration)
idx <- utils::head(if (length(key)) which(idx) else seq_along(idx), mop)

278
R/utils.R
View File

@@ -43,53 +43,43 @@ repeatable <- function(rngfunc, seed = stats::runif(1, 0, .Machine$integer.max))
}
}
# Temporarily set x in env to value, evaluate expr, and
# then restore x to its original state
withTemporary <- function(env, x, value, expr, unset = FALSE) {
if (exists(x, envir = env, inherits = FALSE)) {
oldValue <- get(x, envir = env, inherits = FALSE)
on.exit(
assign(x, oldValue, envir = env, inherits = FALSE),
add = TRUE)
} else {
on.exit(
rm(list = x, envir = env, inherits = FALSE),
add = TRUE
)
}
if (!missing(value) && !isTRUE(unset))
assign(x, value, envir = env, inherits = FALSE)
else {
if (exists(x, envir = env, inherits = FALSE))
rm(list = x, envir = env, inherits = FALSE)
}
force(expr)
}
.globals$ownSeed <- NULL
# Evaluate an expression using Shiny's own private stream of
# randomness (not affected by set.seed).
withPrivateSeed <- function(expr) {
withTemporary(.GlobalEnv, ".Random.seed",
.globals$ownSeed, unset=is.null(.globals$ownSeed), {
tryCatch({
expr
}, finally = {
.globals$ownSeed <- getExists('.Random.seed', 'numeric', globalenv())
})
}
)
}
# Save the old seed if present.
if (exists(".Random.seed", envir = .GlobalEnv, inherits = FALSE)) {
hasOrigSeed <- TRUE
origSeed <- .GlobalEnv$.Random.seed
} else {
hasOrigSeed <- FALSE
}
# a homemade version of set.seed(NULL) for backward compatibility with R 2.15.x
reinitializeSeed <- if (getRversion() >= '3.0.0') {
function() set.seed(NULL)
} else function() {
if (exists('.Random.seed', globalenv()))
rm(list = '.Random.seed', pos = globalenv())
stats::runif(1) # generate any random numbers so R can reinitialize the seed
# Swap in the private seed.
if (is.null(.globals$ownSeed)) {
if (hasOrigSeed) {
# Move old seed out of the way if present.
rm(.Random.seed, envir = .GlobalEnv, inherits = FALSE)
}
} else {
.GlobalEnv$.Random.seed <- .globals$ownSeed
}
# On exit, save the modified private seed, and put the old seed back.
on.exit({
.globals$ownSeed <- .GlobalEnv$.Random.seed
if (hasOrigSeed) {
.GlobalEnv$.Random.seed <- origSeed
} else {
rm(.Random.seed, envir = .GlobalEnv, inherits = FALSE)
}
# Need to call this to make sure that the value of .Random.seed gets put
# into R's internal RNG state. (Issue #1763)
httpuv::getRNGState()
})
expr
}
# Version of runif that runs with private seed
@@ -182,6 +172,16 @@ anyUnnamed <- function(x) {
any(!nzchar(nms))
}
# Given a vector/list, returns a named vector (the labels will be blank).
asNamedVector <- function(x) {
if (!is.null(names(x)))
return(x)
names(x) <- rep.int("", length(x))
x
}
# 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
@@ -196,6 +196,30 @@ mergeVectors <- function(a, b) {
x[!drop_idx]
}
# Sort a vector by the names of items. If there are multiple items with the
# same name, preserve the original order of those items. For empty
# vectors/lists/NULL, return the original value.
sortByName <- function(x) {
if (anyUnnamed(x))
stop("All items must be named")
# Special case for empty vectors/lists, and NULL
if (length(x) == 0)
return(x)
x[order(names(x))]
}
# 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.
list2env2 <- function(x, ...) {
# Ensure that zero-length lists have a name attribute
if (length(x) == 0)
attr(x, "names") <- character(0)
list2env(x, ...)
}
# Combine dir and (file)name into a file path. If a file already exists with a
# name differing only by case, then use it instead.
@@ -245,6 +269,25 @@ dirExists <- function(paths) {
file.exists(paths) & file.info(paths)$isdir
}
# Removes empty directory (vectorized). This is needed because file.remove()
# on Unix will remove empty directories, but on Windows, it will not. On
# Windows, you would need to use unlink(recursive=TRUE), which is not very
# safe. This function does it safely on Unix and Windows.
dirRemove <- function(path) {
for (p in path) {
if (!dirExists(p)) {
stop("Cannot remove non-existent directory ", p, ".")
}
if (length(dir(p, all.files = TRUE, no.. = TRUE)) != 0) {
stop("Cannot remove non-empty directory ", p, ".")
}
result <- unlink(p, recursive = TRUE)
if (result == 1) {
stop("Error removing directory ", p, ".")
}
}
}
# Attempt to join a path and relative path, and turn the result into a
# (normalized) absolute path. The result will only be returned if it is an
# existing file/directory and is a descendant of dir.
@@ -638,6 +681,9 @@ Callbacks <- R6Class(
.callbacks <<- Map$new()
},
register = function(callback) {
if (!is.function(callback)) {
stop("callback must be a function")
}
id <- as.character(.nextId)
.nextId <<- .nextId - 1L
.callbacks$set(id, callback)
@@ -1094,6 +1140,7 @@ reactiveStop <- function(message = "", class = NULL) {
#' @examples
#' ## Only run examples in interactive R sessions
#' if (interactive()) {
#' options(device.ask.default = FALSE)
#'
#' ui <- fluidPage(
#' checkboxGroupInput('in1', 'Check some letters', choices = head(LETTERS)),
@@ -1196,7 +1243,7 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
#' \strong{Truthy and falsy values}
#'
#' The terms "truthy" and "falsy" generally indicate whether a value, when
#' coerced to a \code{\link{logical}}, is \code{TRUE} or \code{FALSE}. We use
#' coerced to a \code{\link[base]{logical}}, is \code{TRUE} or \code{FALSE}. We use
#' the term a little loosely here; our usage tries to match the intuitive
#' notions of "Is this value missing or available?", or "Has the user provided
#' an answer?", or in the case of action buttons, "Has the button been
@@ -1504,7 +1551,10 @@ writeUTF8 <- function(text, ...) {
writeLines(text, ..., useBytes = TRUE)
}
URLdecode <- decodeURIComponent
URLdecode <- function(value) {
decodeURIComponent(value)
}
URLencode <- function(value, reserved = FALSE) {
value <- enc2utf8(value)
if (reserved) encodeURIComponent(value) else encodeURI(value)
@@ -1550,3 +1600,143 @@ Mutable <- R6Class("Mutable",
get = function() { private$value }
)
)
# More convenient way of chaining together promises than then/catch/finally,
# without the performance impact of %...>%.
promise_chain <- function(promise, ..., catch = NULL, finally = NULL,
domain = NULL, replace = FALSE) {
do <- function() {
p <- Reduce(function(memo, func) {
promises::then(memo, func)
}, list(...), promise)
if (!is.null(catch)) {
p <- promises::catch(p, catch)
}
if (!is.null(finally)) {
p <- promises::finally(p, finally)
}
p
}
if (!is.null(domain)) {
promises::with_promise_domain(domain, do(), replace = replace)
} else {
do()
}
}
# Like promise_chain, but if `expr` returns a non-promise, then `...`, `catch`,
# and `finally` are all executed synchronously
hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
domain = NULL, replace = FALSE) {
do <- function() {
runFinally <- TRUE
tryCatch(
{
captureStackTraces({
result <- withVisible(force(expr))
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)
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)
setVisible(result)
}
})
},
error = function(e) {
if (!is.null(catch))
catch(e)
else
stop(e)
},
finally = if (runFinally && !is.null(finally)) finally()
)
}
if (!is.null(domain)) {
promises::with_promise_domain(domain, do(), replace = replace)
} else {
do()
}
}
# 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)
}
}
createVarPromiseDomain <- function(env, name, value) {
force(env)
force(name)
force(value)
promises::new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
function(...) {
orig <- env[[name]]
env[[name]] <- value
on.exit(env[[name]] <- orig)
onFulfilled(...)
}
},
wrapOnRejected = function(onRejected) {
function(...) {
orig <- env[[name]]
env[[name]] <- value
on.exit(env[[name]] <- orig)
onRejected(...)
}
},
wrapSync = function(expr) {
orig <- env[[name]]
env[[name]] <- value
on.exit(env[[name]] <- orig)
force(expr)
}
)
}
getSliderType <- function(min, max, value) {
vals <- dropNulls(list(value, min, max))
type <- unique(lapply(vals, function(x) {
if (inherits(x, "Date")) "date"
else if (inherits(x, "POSIXt")) "datetime"
else "number"
}))
if (length(type) > 1) {
stop("Type mismatch for `min`, `max`, and `value`. Each must be Date, POSIXt, or number.")
}
type[[1]]
}

View File

@@ -8,15 +8,17 @@ Shiny is a new package from RStudio that makes it incredibly easy to build inter
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).
## 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/2.3.2/).
* Attractive default UI theme based on [Bootstrap](http://getbootstrap.com/).
* A highly customizable slider widget with built-in support for animation.
* Pre-built output widgets for displaying plots, tables, and printed output of R objects.
* 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!).
@@ -41,8 +43,6 @@ devtools::install_github("rstudio/shiny")
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.
We hope you enjoy using Shiny. If you have general questions about using Shiny, please use the Shiny [mailing list](https://groups.google.com/forum/#!forum/shiny-discuss). For bug reports, please use the [issue tracker](https://github.com/rstudio/shiny/issues).
## 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.
@@ -59,6 +59,10 @@ devtools::install_version("shiny", version = "0.10.2.2")
The Javascript code in Shiny is minified using tools that run on Node.js. See the tools/ directory for more information.
## Guidelines for contributing
We welcome contributions to the **shiny** package. Please see our [CONTRIBUTING.md](CONTRIBUTING.md) file for detailed guidelines of how to contribute.
## License
The shiny package is licensed under the GPLv3. See these files in the inst directory for additional details:

54
TODO-promises.md Normal file
View File

@@ -0,0 +1,54 @@
# Promises TODO
## Documentation
- [x] Motivation -- why should I care about async? Why shouldn't I (what are the limitations)?
- [x] High level technical overview
- [ ] Cookbook-style examples
- [ ] Top-down porting of a sync app to async
## Core API
- [x] Should as.promise() convert regular values to promises? Or throw?
- [x] If as.promise() doesn't convert regular values to promises, add promise_resolved(value) and promise_rejected(err) functions?
## later
- [ ] Add support for multiple event loops
- [x] Add timeout to run_now
## Error handling/debugging
- [ ] ..stacktraceon../..stacktraceoff.. and stack traces in general
- [x] long stack traces
- [x] require opt-in
- [ ] options(shiny.error) should work in promise handlers
- [x] Detect when reactives are used across process boundaries, and error
## Render functions
- [x] Non-async render functions should have their code all execute on the current tick. Otherwise order of execution will be surprising if they have side effects and explicit priorities.
- [x] Promise domains should maybe have an onExecute, for the "sync" part that kicks off async operations to also have wrapping behavior (like capturing output). Right now, I have to start off renderPrint with promise(~resolve(TRUE)) and then execute the user code in a then(), just to get the promise behavior. Same will be true when we tackle error handling (stack trace capture).
- [x] invisible() doesn't seem to be working correctly with renderPrint. .visible doesn't survive promise chaining, e.g. promise(~resolve(promise(~resolve(invisible("Hi"))))) %>% then(function(x, .visible) { cat(.visible) }) will print TRUE, not FALSE.
- [x] renderDataTable should support async
- [x] Support downloadHandler
- [ ] Support async filename?
- [x] Should prevent session from continuing until download completes (ref count)
## Flush lifecycle
- [x] While async operations are running in a session, hold off on any further processing of inputs and scheduled task items until all operations are complete.
- [x] Hold all outputs/errors until async operations are complete.
- [ ] Allow both sync and async outputs to be displayed before all outputs are done. (opt-in)
## Testing
- [x] App that tests that all built-in render functions support async
- [x] Apps that test flush lifecycle, including onFlushed(once = FALSE)
- [x] Apps that test invisible() behavior for renderPrint, both sync and async
- [x] Apps that ensure all render functions execute synchronous code before tick is over
- [x] App that tests async downloadHandler
- [x] App that verifies inputs/timers don't fire for a session while it has async operations pending
- [x] App that verifies req(FALSE), req(FALSE, cancelOutput = TRUE), validate/need, etc. all work in async
## External packages
- [x] DT
- [x] htmlwidgets: Don't require async-aware version of Shiny if not using async
- [x] Plotly
## Bugs
- [x] req(FALSE, cancelOutput = TRUE) shows grey (even without async)

View File

@@ -10,6 +10,9 @@ init:
install:
ps: Bootstrap
cache:
- C:\RLibrary
# Adapt as necessary starting from here
build_script:
@@ -40,3 +43,7 @@ artifacts:
- path: '\*_*.zip'
name: Bits
environment:
global:
USE_RTOOLS: true

View File

@@ -1,4 +1,3 @@
This small Shiny application demonstrates Shiny's automatic UI updates. Move
the *Number of bins* slider and notice how the `renderPlot` expression is
automatically re-evaluated when its dependant, `input$bins`, changes,
causing a histogram with a new number of bins to be rendered.
This small Shiny application demonstrates Shiny's automatic UI updates.
Move the *Number of bins* slider and notice how the `renderPlot` expression is automatically re-evaluated when its dependant, `input$bins`, changes, causing a histogram with a new number of bins to be rendered.

View File

@@ -0,0 +1,59 @@
library(shiny)
# Define UI for app that draws a histogram ----
ui <- fluidPage(
# App title ----
titlePanel("Hello Shiny!"),
# Sidebar layout with input and output definitions ----
sidebarLayout(
# Sidebar panel for inputs ----
sidebarPanel(
# Input: Slider for the number of bins ----
sliderInput(inputId = "bins",
label = "Number of bins:",
min = 1,
max = 50,
value = 30)
),
# Main panel for displaying outputs ----
mainPanel(
# Output: Histogram ----
plotOutput(outputId = "distPlot")
)
)
)
# Define server logic required to draw a histogram ----
server <- function(input, output) {
# Histogram of the Old Faithful Geyser Data ----
# with requested number of bins
# This expression that generates a histogram is wrapped in a call
# to renderPlot to indicate that:
#
# 1. It is "reactive" and therefore should be automatically
# re-executed when inputs (input$bins) change
# 2. Its output type is a plot
output$distPlot <- renderPlot({
x <- faithful$waiting
bins <- seq(min(x), max(x), length.out = input$bins + 1)
hist(x, breaks = bins, col = "#75AADB", border = "white",
xlab = "Waiting time to next eruption (in mins)",
main = "Histogram of waiting times")
})
}
# Create Shiny app ----
shinyApp(ui = ui, server = server)

View File

@@ -1,6 +0,0 @@
name: 01_hello
account: admin
server: localhost
bundleId: 1
url: http://localhost:3939/admin/01_hello/
when: 1436550957.65385

View File

@@ -1,21 +0,0 @@
library(shiny)
# Define server logic required to draw a histogram
function(input, output) {
# Expression that generates a histogram. The expression is
# wrapped in a call to renderPlot to indicate that:
#
# 1) It is "reactive" and therefore should be automatically
# re-executed when inputs change
# 2) Its output type is a plot
output$distPlot <- renderPlot({
x <- faithful[, 2] # Old Faithful Geyser data
bins <- seq(min(x), max(x), length.out = input$bins + 1)
# draw the histogram with the specified number of bins
hist(x, breaks = bins, col = 'darkgray', border = 'white')
})
}

View File

@@ -1,24 +0,0 @@
library(shiny)
# Define UI for application that draws a histogram
fluidPage(
# Application title
titlePanel("Hello Shiny!"),
# Sidebar with a slider input for the number of bins
sidebarLayout(
sidebarPanel(
sliderInput("bins",
"Number of bins:",
min = 1,
max = 50,
value = 30)
),
# Show a plot of the generated distribution
mainPanel(
plotOutput("distPlot")
)
)
)

View File

@@ -1 +1 @@
This example demonstrates output of raw text from R using the `renderPrint` function in `server.R` and the `verbatimTextOutput` function in `ui.R`. In this case, a textual summary of the data is shown using R's built-in `summary` function.
This example demonstrates output of raw text from R using the `renderPrint` function in `server` and the `verbatimTextOutput` function in `ui`. In this case, a textual summary of the data is shown using R's built-in `summary` function.

View File

@@ -0,0 +1,64 @@
library(shiny)
# Define UI for dataset viewer app ----
ui <- fluidPage(
# App title ----
titlePanel("Shiny Text"),
# Sidebar layout with a input and output definitions ----
sidebarLayout(
# Sidebar panel for inputs ----
sidebarPanel(
# Input: Selector for choosing dataset ----
selectInput(inputId = "dataset",
label = "Choose a dataset:",
choices = c("rock", "pressure", "cars")),
# Input: Numeric entry for number of obs to view ----
numericInput(inputId = "obs",
label = "Number of observations to view:",
value = 10)
),
# Main panel for displaying outputs ----
mainPanel(
# Output: Verbatim text for data summary ----
verbatimTextOutput("summary"),
# Output: HTML table with requested number of observations ----
tableOutput("view")
)
)
)
# Define server logic to summarize and view selected dataset ----
server <- function(input, output) {
# Return the requested dataset ----
datasetInput <- reactive({
switch(input$dataset,
"rock" = rock,
"pressure" = pressure,
"cars" = cars)
})
# Generate a summary of the dataset ----
output$summary <- renderPrint({
dataset <- datasetInput()
summary(dataset)
})
# Show the first "n" observations ----
output$view <- renderTable({
head(datasetInput(), n = input$obs)
})
}
# Create Shiny app ----
shinyApp(ui = ui, server = server)

View File

@@ -1,26 +0,0 @@
library(shiny)
library(datasets)
# Define server logic required to summarize and view the selected
# dataset
function(input, output) {
# Return the requested dataset
datasetInput <- reactive({
switch(input$dataset,
"rock" = rock,
"pressure" = pressure,
"cars" = cars)
})
# Generate a summary of the dataset
output$summary <- renderPrint({
dataset <- datasetInput()
summary(dataset)
})
# Show the first "n" observations
output$view <- renderTable({
head(datasetInput(), n = input$obs)
})
}

View File

@@ -1,27 +0,0 @@
library(shiny)
# Define UI for dataset viewer application
fluidPage(
# Application title
titlePanel("Shiny Text"),
# Sidebar with controls to select a dataset and specify the
# number of observations to view
sidebarLayout(
sidebarPanel(
selectInput("dataset", "Choose a dataset:",
choices = c("rock", "pressure", "cars")),
numericInput("obs", "Number of observations to view:", 10)
),
# Show a summary of the dataset and an HTML table with the
# requested number of observations
mainPanel(
verbatimTextOutput("summary"),
tableOutput("view")
)
)
)

View File

@@ -1,5 +1,5 @@
This example demonstrates a core feature of Shiny: **reactivity**. In `server.R`, a reactive called `datasetInput` is declared.
This example demonstrates a core feature of Shiny: **reactivity**. In the `server` function, a reactive called `datasetInput` is declared.
Notice that the reactive expression depends on the input expression `input$dataset`, and that it's used by both the output expression `output$summary` and `output$view`. Try changing the dataset (using *Choose a dataset*) while looking at the reactive and then at the outputs; you will see first the reactive and then its dependencies flash.
Notice that the reactive expression depends on the input expression `input$dataset`, and that it's used by two output expressions: `output$summary` and `output$view`. Try changing the dataset (using *Choose a dataset*) while looking at the reactive and then at the outputs; you will see first the reactive and then its dependencies flash.
Notice also that the reactive expression doesn't just update whenever anything changes--only the inputs it depends on will trigger an update. Change the "Caption" field and notice how only the `output$caption` expression is re-evaluated; the reactive and its dependents are left alone.

View File

@@ -0,0 +1,102 @@
library(shiny)
# Define UI for dataset viewer app ----
ui <- fluidPage(
# App title ----
titlePanel("Reactivity"),
# Sidebar layout with input and output definitions ----
sidebarLayout(
# Sidebar panel for inputs ----
sidebarPanel(
# Input: Text for providing a caption ----
# Note: Changes made to the caption in the textInput control
# are updated in the output area immediately as you type
textInput(inputId = "caption",
label = "Caption:",
value = "Data Summary"),
# Input: Selector for choosing dataset ----
selectInput(inputId = "dataset",
label = "Choose a dataset:",
choices = c("rock", "pressure", "cars")),
# Input: Numeric entry for number of obs to view ----
numericInput(inputId = "obs",
label = "Number of observations to view:",
value = 10)
),
# Main panel for displaying outputs ----
mainPanel(
# Output: Formatted text for caption ----
h3(textOutput("caption", container = span)),
# Output: Verbatim text for data summary ----
verbatimTextOutput("summary"),
# Output: HTML table with requested number of observations ----
tableOutput("view")
)
)
)
# Define server logic to summarize and view selected dataset ----
server <- function(input, output) {
# Return the requested dataset ----
# By declaring datasetInput as a reactive expression we ensure
# that:
#
# 1. It is only called when the inputs it depends on changes
# 2. The computation and result are shared by all the callers,
# i.e. it only executes a single time
datasetInput <- reactive({
switch(input$dataset,
"rock" = rock,
"pressure" = pressure,
"cars" = cars)
})
# Create caption ----
# The output$caption is computed based on a reactive expression
# that returns input$caption. When the user changes the
# "caption" field:
#
# 1. This function is automatically called to recompute the output
# 2. New caption is pushed back to the browser for re-display
#
# Note that because the data-oriented reactive expressions
# below don't depend on input$caption, those expressions are
# NOT called when input$caption changes
output$caption <- renderText({
input$caption
})
# Generate a summary of the dataset ----
# The output$summary depends on the datasetInput reactive
# expression, so will be re-executed whenever datasetInput is
# invalidated, i.e. whenever the input$dataset changes
output$summary <- renderPrint({
dataset <- datasetInput()
summary(dataset)
})
# Show the first "n" observations ----
# The output$view depends on both the databaseInput reactive
# expression and input$obs, so it will be re-executed whenever
# input$dataset or input$obs is changed
output$view <- renderTable({
head(datasetInput(), n = input$obs)
})
}
# Create Shiny app ----
shinyApp(ui, server)

View File

@@ -1,53 +0,0 @@
library(shiny)
library(datasets)
# Define server logic required to summarize and view the selected
# dataset
function(input, output) {
# By declaring datasetInput as a reactive expression we ensure
# that:
#
# 1) It is only called when the inputs it depends on changes
# 2) The computation and result are shared by all the callers
# (it only executes a single time)
#
datasetInput <- reactive({
switch(input$dataset,
"rock" = rock,
"pressure" = pressure,
"cars" = cars)
})
# The output$caption is computed based on a reactive expression
# that returns input$caption. When the user changes the
# "caption" field:
#
# 1) This function is automatically called to recompute the
# output
# 2) The new caption is pushed back to the browser for
# re-display
#
# Note that because the data-oriented reactive expressions
# below don't depend on input$caption, those expressions are
# NOT called when input$caption changes.
output$caption <- renderText({
input$caption
})
# The output$summary depends on the datasetInput reactive
# expression, so will be re-executed whenever datasetInput is
# invalidated
# (i.e. whenever the input$dataset changes)
output$summary <- renderPrint({
dataset <- datasetInput()
summary(dataset)
})
# The output$view depends on both the databaseInput reactive
# expression and input$obs, so will be re-executed whenever
# input$dataset or input$obs is changed.
output$view <- renderTable({
head(datasetInput(), n = input$obs)
})
}

View File

@@ -1,34 +0,0 @@
library(shiny)
# Define UI for dataset viewer application
fluidPage(
# Application title
titlePanel("Reactivity"),
# Sidebar with controls to provide a caption, select a dataset,
# and specify the number of observations to view. Note that
# changes made to the caption in the textInput control are
# updated in the output area immediately as you type
sidebarLayout(
sidebarPanel(
textInput("caption", "Caption:", "Data Summary"),
selectInput("dataset", "Choose a dataset:",
choices = c("rock", "pressure", "cars")),
numericInput("obs", "Number of observations to view:", 10)
),
# Show the caption, a summary of the dataset and an HTML
# table with the requested number of observations
mainPanel(
h3(textOutput("caption", container = span)),
verbatimTextOutput("summary"),
tableOutput("view")
)
)
)

View File

@@ -1,4 +1,4 @@
This example demonstrates the following concepts:
* **Global variables**: The `mpgData` variable is declared outside the `shinyServer` function. This makes it available anywhere inside `shinyServer`. The code in `server.R` outside `shinyServer` is only run once when the app starts up, so it can't contain user input.
* **Reactive expressions**: `formulaText` is a reactive expression. Note how it re-evaluates when the Variable field is changed, but not when the Show Outliers box is ticked.
- **Global variables**: The `mpgData` variable is declared outside of the `ui` and `server` function definitions. This makes it available anywhere inside `app.R`. The code in `app.R` outside of `ui` and `server` function definitions is only run once when the app starts up, so it can't contain user input.
- **Reactive expressions**: `formulaText` is a reactive expression. Note how it re-evaluates when the Variable field is changed, but not when the Show Outliers box is unchecked.

View File

@@ -0,0 +1,75 @@
library(shiny)
library(datasets)
# Data pre-processing ----
# Tweak the "am" variable to have nicer factor labels -- since this
# doesn't rely on any user inputs, we can do this once at startup
# and then use the value throughout the lifetime of the app
mpgData <- mtcars
mpgData$am <- factor(mpgData$am, labels = c("Automatic", "Manual"))
# Define UI for miles per gallon app ----
ui <- fluidPage(
# App title ----
titlePanel("Miles Per Gallon"),
# Sidebar layout with input and output definitions ----
sidebarLayout(
# Sidebar panel for inputs ----
sidebarPanel(
# Input: Selector for variable to plot against mpg ----
selectInput("variable", "Variable:",
c("Cylinders" = "cyl",
"Transmission" = "am",
"Gears" = "gear")),
# Input: Checkbox for whether outliers should be included ----
checkboxInput("outliers", "Show outliers", TRUE)
),
# Main panel for displaying outputs ----
mainPanel(
# Output: Formatted text for caption ----
h3(textOutput("caption")),
# Output: Plot of the requested variable against mpg ----
plotOutput("mpgPlot")
)
)
)
# Define server logic to plot various variables against mpg ----
server <- function(input, output) {
# Compute the formula text ----
# This is in a reactive expression since it is shared by the
# output$caption and output$mpgPlot functions
formulaText <- reactive({
paste("mpg ~", input$variable)
})
# Return the formula text for printing as a caption ----
output$caption <- renderText({
formulaText()
})
# Generate a plot of the requested variable against mpg ----
# and only exclude outliers if requested
output$mpgPlot <- renderPlot({
boxplot(as.formula(formulaText()),
data = mpgData,
outline = input$outliers,
col = "#75AADB", pch = 19)
})
}
# Create Shiny app ----
shinyApp(ui, server)

View File

@@ -1,34 +0,0 @@
library(shiny)
library(datasets)
# We tweak the "am" field to have nicer factor labels. Since
# this doesn't rely on any user inputs we can do this once at
# startup and then use the value throughout the lifetime of the
# application
mpgData <- mtcars
mpgData$am <- factor(mpgData$am, labels = c("Automatic", "Manual"))
# Define server logic required to plot various variables against
# mpg
function(input, output) {
# Compute the formula text in a reactive expression since it is
# shared by the output$caption and output$mpgPlot functions
formulaText <- reactive({
paste("mpg ~", input$variable)
})
# Return the formula text for printing as a caption
output$caption <- renderText({
formulaText()
})
# Generate a plot of the requested variable against mpg and
# only include outliers if requested
output$mpgPlot <- renderPlot({
boxplot(as.formula(formulaText()),
data = mpgData,
outline = input$outliers)
})
}

View File

@@ -1,29 +0,0 @@
library(shiny)
# Define UI for miles per gallon application
fluidPage(
# Application title
titlePanel("Miles Per Gallon"),
# Sidebar with controls to select the variable to plot against
# mpg and to specify whether outliers should be included
sidebarLayout(
sidebarPanel(
selectInput("variable", "Variable:",
c("Cylinders" = "cyl",
"Transmission" = "am",
"Gears" = "gear")),
checkboxInput("outliers", "Show outliers", FALSE)
),
# Show the caption and plot of the requested variable against
# mpg
mainPanel(
h3(textOutput("caption")),
plotOutput("mpgPlot")
)
)
)

View File

@@ -0,0 +1,86 @@
library(shiny)
# Define UI for slider demo app ----
ui <- fluidPage(
# App title ----
titlePanel("Sliders"),
# Sidebar layout with input and output definitions ----
sidebarLayout(
# Sidebar to demonstrate various slider options ----
sidebarPanel(
# Input: Simple integer interval ----
sliderInput("integer", "Integer:",
min = 0, max = 1000,
value = 500),
# Input: Decimal interval with step value ----
sliderInput("decimal", "Decimal:",
min = 0, max = 1,
value = 0.5, step = 0.1),
# Input: Specification of range within an interval ----
sliderInput("range", "Range:",
min = 1, max = 1000,
value = c(200,500)),
# Input: Custom currency format for with basic animation ----
sliderInput("format", "Custom Format:",
min = 0, max = 10000,
value = 0, step = 2500,
pre = "$", sep = ",",
animate = TRUE),
# Input: Animation with custom interval (in ms) ----
# to control speed, plus looping
sliderInput("animation", "Looping Animation:",
min = 1, max = 2000,
value = 1, step = 10,
animate =
animationOptions(interval = 300, loop = TRUE))
),
# Main panel for displaying outputs ----
mainPanel(
# Output: Table summarizing the values entered ----
tableOutput("values")
)
)
)
# Define server logic for slider examples ----
server <- function(input, output) {
# Reactive expression to create data frame of all input values ----
sliderValues <- reactive({
data.frame(
Name = c("Integer",
"Decimal",
"Range",
"Custom Format",
"Animation"),
Value = as.character(c(input$integer,
input$decimal,
paste(input$range, collapse = " "),
input$format,
input$animation)),
stringsAsFactors = FALSE)
})
# Show the values in an HTML table ----
output$values <- renderTable({
sliderValues()
})
}
# Create Shiny app ----
shinyApp(ui, server)

View File

@@ -1,29 +0,0 @@
library(shiny)
# Define server logic for slider examples
function(input, output) {
# Reactive expression to compose a data frame containing all of
# the values
sliderValues <- reactive({
# Compose data frame
data.frame(
Name = c("Integer",
"Decimal",
"Range",
"Custom Format",
"Animation"),
Value = as.character(c(input$integer,
input$decimal,
paste(input$range, collapse=' '),
input$format,
input$animation)),
stringsAsFactors=FALSE)
})
# Show the values using an HTML table
output$values <- renderTable({
sliderValues()
})
}

View File

@@ -1,43 +0,0 @@
library(shiny)
# Define UI for slider demo application
fluidPage(
# Application title
titlePanel("Sliders"),
# Sidebar with sliders that demonstrate various available
# options
sidebarLayout(
sidebarPanel(
# Simple integer interval
sliderInput("integer", "Integer:",
min=0, max=1000, value=500),
# Decimal interval with step value
sliderInput("decimal", "Decimal:",
min = 0, max = 1, value = 0.5, step= 0.1),
# Specification of range within an interval
sliderInput("range", "Range:",
min = 1, max = 1000, value = c(200,500)),
# Provide a custom currency format for value display,
# with basic animation
sliderInput("format", "Custom Format:",
min = 0, max = 10000, value = 0, step = 2500,
pre = "$", sep = ",", animate=TRUE),
# Animation with custom interval (in ms) to control speed,
# plus looping
sliderInput("animation", "Looping Animation:", 1, 2000, 1,
step = 10, animate=
animationOptions(interval=300, loop=TRUE))
),
# Show a table summarizing the values entered
mainPanel(
tableOutput("values")
)
)
)

View File

@@ -2,7 +2,7 @@ This example demonstrates the `tabsetPanel` and `tabPanel` widgets.
Notice that outputs that are not visible are not re-evaluated until they become visible. Try this:
1. Scroll to the bottom of `server.R`
1. Scroll to the bottom of the `server` function. You might need to use the *show with app* option so you can easily view the code and interact with the app at the same time.
2. Change the number of observations, and observe that only `output$plot` is evaluated.
3. Click the Summary tab, and observe that `output$summary` is evaluated.
4. Change the number of observations again, and observe that now only `output$summary` is evaluated.

View File

@@ -0,0 +1,92 @@
library(shiny)
# Define UI for random distribution app ----
ui <- fluidPage(
# App title ----
titlePanel("Tabsets"),
# Sidebar layout with input and output definitions ----
sidebarLayout(
# Sidebar panel for inputs ----
sidebarPanel(
# Input: Select the random distribution type ----
radioButtons("dist", "Distribution type:",
c("Normal" = "norm",
"Uniform" = "unif",
"Log-normal" = "lnorm",
"Exponential" = "exp")),
# br() element to introduce extra vertical spacing ----
br(),
# Input: Slider for the number of observations to generate ----
sliderInput("n",
"Number of observations:",
value = 500,
min = 1,
max = 1000)
),
# Main panel for displaying outputs ----
mainPanel(
# Output: Tabset w/ plot, summary, and table ----
tabsetPanel(type = "tabs",
tabPanel("Plot", plotOutput("plot")),
tabPanel("Summary", verbatimTextOutput("summary")),
tabPanel("Table", tableOutput("table"))
)
)
)
)
# Define server logic for random distribution app ----
server <- function(input, output) {
# Reactive expression to generate the requested distribution ----
# This is called whenever the inputs change. The output functions
# defined below then use the value computed from this expression
d <- reactive({
dist <- switch(input$dist,
norm = rnorm,
unif = runif,
lnorm = rlnorm,
exp = rexp,
rnorm)
dist(input$n)
})
# Generate a plot of the data ----
# Also uses the inputs to build the plot label. Note that the
# dependencies on the inputs and the data reactive expression are
# both tracked, and all expressions are called in the sequence
# implied by the dependency graph.
output$plot <- renderPlot({
dist <- input$dist
n <- input$n
hist(d(),
main = paste("r", dist, "(", n, ")", sep = ""),
col = "#75AADB", border = "white")
})
# Generate a summary of the data ----
output$summary <- renderPrint({
summary(d())
})
# Generate an HTML table view of the data ----
output$table <- renderTable({
d()
})
}
# Create Shiny app ----
shinyApp(ui, server)

View File

@@ -1,44 +0,0 @@
library(shiny)
# Define server logic for random distribution application
function(input, output) {
# Reactive expression to generate the requested distribution.
# This is called whenever the inputs change. The output
# functions defined below then all use the value computed from
# this expression
data <- reactive({
dist <- switch(input$dist,
norm = rnorm,
unif = runif,
lnorm = rlnorm,
exp = rexp,
rnorm)
dist(input$n)
})
# Generate a plot of the data. Also uses the inputs to build
# the plot label. Note that the dependencies on both the inputs
# and the data reactive expression are both tracked, and
# all expressions are called in the sequence implied by the
# dependency graph
output$plot <- renderPlot({
dist <- input$dist
n <- input$n
hist(data(),
main=paste('r', dist, '(', n, ')', sep=''))
})
# Generate a summary of the data
output$summary <- renderPrint({
summary(data())
})
# Generate an HTML table view of the data
output$table <- renderTable({
data.frame(x=data())
})
}

View File

@@ -1,38 +0,0 @@
library(shiny)
# Define UI for random distribution application
fluidPage(
# Application title
titlePanel("Tabsets"),
# Sidebar with controls to select the random distribution type
# and number of observations to generate. Note the use of the
# br() element to introduce extra vertical spacing
sidebarLayout(
sidebarPanel(
radioButtons("dist", "Distribution type:",
c("Normal" = "norm",
"Uniform" = "unif",
"Log-normal" = "lnorm",
"Exponential" = "exp")),
br(),
sliderInput("n",
"Number of observations:",
value = 500,
min = 1,
max = 1000)
),
# Show a tabset that includes a plot, summary, and table view
# of the generated distribution
mainPanel(
tabsetPanel(type = "tabs",
tabPanel("Plot", plotOutput("plot")),
tabPanel("Summary", verbatimTextOutput("summary")),
tabPanel("Table", tableOutput("table"))
)
)
)
)

View File

@@ -1 +1 @@
This example demonstrates some additional widgets included in Shiny, such as `helpText` and `submitButton`. The latter is used to delay rendering output until the user explicitly requests it.
This example demonstrates some additional widgets included in Shiny, such as `helpText` and `actionButton`. The latter is used to delay rendering output until the user explicitly requests it (a construct which also introduces two important server functions, `eventReactive` and `isolate`).

View File

@@ -0,0 +1,82 @@
library(shiny)
# Define UI for dataset viewer app ----
ui <- fluidPage(
# App title ----
titlePanel("More Widgets"),
# Sidebar layout with input and output definitions ----
sidebarLayout(
# Sidebar panel for inputs ----
sidebarPanel(
# Input: Select a dataset ----
selectInput("dataset", "Choose a dataset:",
choices = c("rock", "pressure", "cars")),
# Input: Specify the number of observations to view ----
numericInput("obs", "Number of observations to view:", 10),
# Include clarifying text ----
helpText("Note: while the data view will show only the specified",
"number of observations, the summary will still be based",
"on the full dataset."),
# Input: actionButton() to defer the rendering of output ----
# until the user explicitly clicks the button (rather than
# doing it immediately when inputs change). This is useful if
# the computations required to render output are inordinately
# time-consuming.
actionButton("update", "Update View")
),
# Main panel for displaying outputs ----
mainPanel(
# Output: Header + summary of distribution ----
h4("Summary"),
verbatimTextOutput("summary"),
# Output: Header + table of distribution ----
h4("Observations"),
tableOutput("view")
)
)
)
# Define server logic to summarize and view selected dataset ----
server <- function(input, output) {
# Return the requested dataset ----
# Note that we use eventReactive() here, which depends on
# input$update (the action button), so that the output is only
# updated when the user clicks the button
datasetInput <- eventReactive(input$update, {
switch(input$dataset,
"rock" = rock,
"pressure" = pressure,
"cars" = cars)
}, ignoreNULL = FALSE)
# Generate a summary of the dataset ----
output$summary <- renderPrint({
dataset <- datasetInput()
summary(dataset)
})
# Show the first "n" observations ----
# The use of isolate() is necessary because we don't want the table
# to update whenever input$obs changes (only when the user clicks
# the action button)
output$view <- renderTable({
head(datasetInput(), n = isolate(input$obs))
})
}
# Create Shiny app ----
shinyApp(ui, server)

View File

@@ -1,26 +0,0 @@
library(shiny)
library(datasets)
# Define server logic required to summarize and view the
# selected dataset
function(input, output) {
# Return the requested dataset
datasetInput <- reactive({
switch(input$dataset,
"rock" = rock,
"pressure" = pressure,
"cars" = cars)
})
# Generate a summary of the dataset
output$summary <- renderPrint({
dataset <- datasetInput()
summary(dataset)
})
# Show the first "n" observations
output$view <- renderTable({
head(datasetInput(), n = input$obs)
})
}

View File

@@ -1,43 +0,0 @@
library(shiny)
# Define UI for dataset viewer application
fluidPage(
# Application title.
titlePanel("More Widgets"),
# Sidebar with controls to select a dataset and specify the
# number of observations to view. The helpText function is
# also used to include clarifying text. Most notably, the
# inclusion of a submitButton defers the rendering of output
# until the user explicitly clicks the button (rather than
# doing it immediately when inputs change). This is useful if
# the computations required to render output are inordinately
# time-consuming.
sidebarLayout(
sidebarPanel(
selectInput("dataset", "Choose a dataset:",
choices = c("rock", "pressure", "cars")),
numericInput("obs", "Number of observations to view:", 10),
helpText("Note: while the data view will show only the specified",
"number of observations, the summary will still be based",
"on the full dataset."),
submitButton("Update View")
),
# Show a summary of the dataset and an HTML table with the
# requested number of observations. Note the use of the h4
# function to provide an additional header above each output
# section.
mainPanel(
h4("Summary"),
verbatimTextOutput("summary"),
h4("Observations"),
tableOutput("view")
)
)
)

View File

@@ -1,4 +1 @@
Normally we use the built-in functions, such as `textInput()`, to generate
the HTML UI in the R script `ui.R`. Actually **shiny** also works with a
custom HTML page `www/index.html`. See [the
tutorial](http://rstudio.github.io/shiny/tutorial/#html-ui) for more details.
Normally we use the built-in functions, such as `textInput()`, to generate the HTML UI in the R script `ui.R`. Actually **shiny** also works with a custom HTML page `www/index.html`. See [the tutorial](http://shiny.rstudio.com/tutorial/) for more details.

View File

@@ -0,0 +1,47 @@
library(shiny)
# Define server logic for random distribution app ----
server <- function(input, output) {
# Reactive expression to generate the requested distribution ----
# This is called whenever the inputs change. The output functions
# defined below then use the value computed from this expression
d <- reactive({
dist <- switch(input$dist,
norm = rnorm,
unif = runif,
lnorm = rlnorm,
exp = rexp,
rnorm)
dist(input$n)
})
# Generate a plot of the data ----
# Also uses the inputs to build the plot label. Note that the
# dependencies on the inputs and the data reactive expression are
# both tracked, and all expressions are called in the sequence
# implied by the dependency graph.
output$plot <- renderPlot({
dist <- input$dist
n <- input$n
hist(d(),
main = paste("r", dist, "(", n, ")", sep = ""),
col = "#75AADB", border = "white")
})
# Generate a summary of the data ----
output$summary <- renderPrint({
summary(d())
})
# Generate an HTML table view of the head of the data ----
output$table <- renderTable({
head(data.frame(x = d()))
})
}
# Create Shiny app ----
shinyApp(ui = htmlTemplate("www/index.html"), server)

View File

@@ -1,42 +0,0 @@
library(shiny)
# Define server logic for random distribution application
function(input, output) {
# Reactive expression to generate the requested distribution. This is
# called whenever the inputs change. The output expressions defined
# below then all used the value computed from this expression
data <- reactive({
dist <- switch(input$dist,
norm = rnorm,
unif = runif,
lnorm = rlnorm,
exp = rexp,
rnorm)
dist(input$n)
})
# Generate a plot of the data. Also uses the inputs to build the
# plot label. Note that the dependencies on both the inputs and
# the data reactive expression are both tracked, and all expressions
# are called in the sequence implied by the dependency graph
output$plot <- renderPlot({
dist <- input$dist
n <- input$n
hist(data(),
main=paste('r', dist, '(', n, ')', sep=''))
})
# Generate a summary of the data
output$summary <- renderPrint({
summary(data())
})
# Generate an HTML table view of the data
output$table <- renderTable({
data.frame(x=data())
})
}

View File

@@ -3,13 +3,13 @@
<head>
<script src="shared/jquery.js" type="text/javascript"></script>
<script src="shared/shiny.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="shared/shiny.css"/>
<link rel="stylesheet" type="text/css" href="shared/shiny.css"/>
</head>
<body>
<h1>HTML UI</h1>
<p>
<label>Distribution type:</label><br />
<select name="dist">
@@ -17,22 +17,25 @@
<option value="unif">Uniform</option>
<option value="lnorm">Log-normal</option>
<option value="exp">Exponential</option>
</select>
</select>
</p>
<p>
<label>Number of observations:</label><br />
<label>Number of observations:</label><br />
<input type="number" name="n" value="500" min="1" max="1000" />
</p>
<pre id="summary" class="shiny-text-output"></pre>
<div id="plot" class="shiny-plot-output"
style="width: 100%; height: 400px"></div>
<h3>Summary of data:</h3>
<pre id="summary" class="shiny-text-output"></pre>
<h3>Plot of data:</h3>
<div id="plot" class="shiny-plot-output"
style="width: 100%; height: 300px"></div>
<h3>Head of data:</h3>
<div id="table" class="shiny-html-output"></div>
</body>
</html>
</html>

View File

@@ -1,4 +1,3 @@
We can add a file upload input in the UI using the function `fileInput()`,
e.g. `fileInput('foo')`. In `server.R`, we can access the uploaded files via
`input$foo`. See [the
tutorial](http://rstudio.github.io/shiny/tutorial/#uploads) for more details.
e.g. `fileInput('foo')`. In the `server` function, we can access the
uploaded files via `input$foo`.

View File

@@ -0,0 +1,92 @@
library(shiny)
# Define UI for data upload app ----
ui <- fluidPage(
# App title ----
titlePanel("Uploading Files"),
# Sidebar layout with input and output definitions ----
sidebarLayout(
# Sidebar panel for inputs ----
sidebarPanel(
# Input: Select a file ----
fileInput("file1", "Choose CSV File",
multiple = TRUE,
accept = c("text/csv",
"text/comma-separated-values,text/plain",
".csv")),
# Horizontal line ----
tags$hr(),
# Input: Checkbox if file has header ----
checkboxInput("header", "Header", TRUE),
# Input: Select separator ----
radioButtons("sep", "Separator",
choices = c(Comma = ",",
Semicolon = ";",
Tab = "\t"),
selected = ","),
# Input: Select quotes ----
radioButtons("quote", "Quote",
choices = c(None = "",
"Double Quote" = '"',
"Single Quote" = "'"),
selected = '"'),
# Horizontal line ----
tags$hr(),
# Input: Select number of rows to display ----
radioButtons("disp", "Display",
choices = c(Head = "head",
All = "all"),
selected = "head")
),
# Main panel for displaying outputs ----
mainPanel(
# Output: Data file ----
tableOutput("contents")
)
)
)
# Define server logic to read selected file ----
server <- function(input, output) {
output$contents <- renderTable({
# input$file1 will be NULL initially. After the user selects
# and uploads a file, head of that data file by default,
# or all rows if selected, will be shown.
req(input$file1)
df <- read.csv(input$file1$datapath,
header = input$header,
sep = input$sep,
quote = input$quote)
if(input$disp == "head") {
return(head(df))
}
else {
return(df)
}
})
}
# Create Shiny app ----
shinyApp(ui, server)

View File

@@ -1,20 +0,0 @@
library(shiny)
function(input, output) {
output$contents <- renderTable({
# input$file1 will be NULL initially. After the user selects
# and uploads a file, it will be a data frame with 'name',
# 'size', 'type', and 'datapath' columns. The 'datapath'
# column will contain the local filenames where the data can
# be found.
inFile <- input$file1
if (is.null(inFile))
return(NULL)
read.csv(inFile$datapath, header=input$header, sep=input$sep,
quote=input$quote)
})
}

View File

@@ -1,28 +0,0 @@
library(shiny)
fluidPage(
titlePanel("Uploading Files"),
sidebarLayout(
sidebarPanel(
fileInput('file1', 'Choose CSV File',
accept=c('text/csv',
'text/comma-separated-values,text/plain',
'.csv')),
tags$hr(),
checkboxInput('header', 'Header', TRUE),
radioButtons('sep', 'Separator',
c(Comma=',',
Semicolon=';',
Tab='\t'),
','),
radioButtons('quote', 'Quote',
c(None='',
'Double Quote'='"',
'Single Quote'="'"),
'"')
),
mainPanel(
tableOutput('contents')
)
)
)

View File

@@ -1,4 +1,2 @@
We can add a download button to the UI using `downloadButton()`, and write
the content of the file in `downloadHandler()` in `server.R`. See [the
tutorial](http://rstudio.github.io/shiny/tutorial/#downloads) for more
details.
the content of the file in `downloadHandler()` in the `server` function.

View File

@@ -0,0 +1,63 @@
library(shiny)
# Define UI for data download app ----
ui <- fluidPage(
# App title ----
titlePanel("Downloading Data"),
# Sidebar layout with input and output definitions ----
sidebarLayout(
# Sidebar panel for inputs ----
sidebarPanel(
# Input: Choose dataset ----
selectInput("dataset", "Choose a dataset:",
choices = c("rock", "pressure", "cars")),
# Button
downloadButton("downloadData", "Download")
),
# Main panel for displaying outputs ----
mainPanel(
tableOutput("table")
)
)
)
# Define server logic to display and download selected file ----
server <- function(input, output) {
# Reactive value for selected dataset ----
datasetInput <- reactive({
switch(input$dataset,
"rock" = rock,
"pressure" = pressure,
"cars" = cars)
})
# Table of selected dataset ----
output$table <- renderTable({
datasetInput()
})
# Downloadable csv of selected dataset ----
output$downloadData <- downloadHandler(
filename = function() {
paste(input$dataset, ".csv", sep = "")
},
content = function(file) {
write.csv(datasetInput(), file, row.names = FALSE)
}
)
}
# Create Shiny app ----
shinyApp(ui, server)

View File

@@ -1,21 +0,0 @@
function(input, output) {
datasetInput <- reactive({
switch(input$dataset,
"rock" = rock,
"pressure" = pressure,
"cars" = cars)
})
output$table <- renderTable({
datasetInput()
})
output$downloadData <- downloadHandler(
filename = function() {
paste(input$dataset, '.csv', sep='')
},
content = function(file) {
write.csv(datasetInput(), file)
}
)
}

View File

@@ -1,13 +0,0 @@
fluidPage(
titlePanel('Downloading Data'),
sidebarLayout(
sidebarPanel(
selectInput("dataset", "Choose a dataset:",
choices = c("rock", "pressure", "cars")),
downloadButton('downloadData', 'Download')
),
mainPanel(
tableOutput('table')
)
)
)

View File

@@ -0,0 +1,21 @@
library(shiny)
# Define UI for displaying current time ----
ui <- fluidPage(
h2(textOutput("currentTime"))
)
# Define server logic to show current time, update every second ----
server <- function(input, output, session) {
output$currentTime <- renderText({
invalidateLater(1000, session)
paste("The current time is", Sys.time())
})
}
# Create Shiny app ----
shinyApp(ui, server)

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