Compare commits

...

332 Commits

Author SHA1 Message Date
Winston Chang
349de7060f Disable reactlog from reactives.R 2019-05-09 10:21:49 -05:00
Winston Chang
38d2809131 Convert MemoryCache to use fastmap 2019-05-09 10:20:33 -05:00
Winston Chang
d7718991a6 Import fastmap::fastmap 2019-05-09 10:20:33 -05:00
Winston Chang
32c2bff6eb Convert ReactiveValues$.metadata to use Map 2019-05-09 10:20:33 -05:00
Winston Chang
555ede03ed Convert ReactiveValues$.values to use Map 2019-05-08 20:33:52 -05:00
Winston Chang
2a6f218700 Convert ReactiveValues$.dependents to use Map 2019-05-08 20:33:52 -05:00
Winston Chang
b087c19b52 Use fastmap as backing store for Map class 2019-05-08 20:33:52 -05:00
Carson Sievert
6fed1c60ac update news (should've been done in #2404) 2019-05-08 16:36:46 -05:00
Carson Sievert
b10f2a5291 yarn build 2019-05-08 16:30:59 -05:00
Winston Chang
a4a49a354e Merge pull request #2404 from rstudio/inputRateName
Fix issue with input rate policies
2019-05-08 16:24:02 -05:00
Carson Sievert
ead23528ca doSetInput calls setInput (duh) so should have name and type 2019-05-08 16:19:21 -05:00
Carson Sievert
b8644949cc camelCase for consistency; clarify comment 2019-05-08 16:19:13 -05:00
Carson Sievert
b88e3a64f2 comment on the difference between name_type and name 2019-05-08 16:19:07 -05:00
Carson Sievert
2871b423fd rename name arg to name_type where relevant in input decorators...
this will help to highlight when you should call a method with just the input name instead of both the name and the type
2019-05-08 16:19:02 -05:00
Carson Sievert
562fafbc39 pass inputName to immediateCall() and normalCall() 2019-05-08 16:18:55 -05:00
Carson Sievert
191e0874f8 type is only relevant for public methods setInput() and setRatePolicy()
change the name of these arguments to reflect this (name_type)
2019-05-08 16:18:47 -05:00
Carson Sievert
fa5ff7bfa5 Consistently ignore input type in all InputRateDecorator methods 2019-05-08 16:18:39 -05:00
Carson Sievert
82e80ccdeb InputRateDecorator's setInput method needs to strip of the input's ttype before looking up the input's rate policy, closes #2387 2019-05-08 16:18:19 -05:00
Carson Sievert
ff84cf5a18 update news (#2428) 2019-05-08 16:08:20 -05:00
Winston Chang
44843a7768 Merge pull request #2406 from rstudio/null-label
Input label updating
2019-05-08 15:52:09 -05:00
Carson Sievert
68eeb338da Have input labels always include 'control-label' class 2019-05-08 15:15:09 -05:00
Carson Sievert
ea54c17902 merge with master 2019-05-08 15:10:22 -05:00
Barret Schloerke
d5ad7eed40 Merge pull request #2424 from rstudio/joe/bugfix/reactive-value-not-changing
Fix rstudio/reactlog#36: Changes to reactive values not displaying accurately
2019-05-08 12:01:08 -04:00
Joe Cheng
c2430cd3f4 Update NEWS 2019-05-07 09:33:59 -07:00
Joe Cheng
8a0731493f Fix rstudio/reactlog#36: Changes to reactive values not displaying accurately 2019-05-07 09:30:59 -07:00
Carson Sievert
07e2b80b5d merge with master; fix NEWS conflicts 2019-05-03 17:21:06 -05:00
Carson Sievert
1311e1fca2 have class come before the for attribute 2019-05-03 17:16:17 -05:00
Winston Chang
e6c2133520 Merge pull request #2416 from rstudio/updateSliderInput
getSliderType() should be able to handle NULL min/max/value, fixes #2250
2019-05-03 16:54:57 -05:00
Carson Sievert
3d6f734ff2 update comment 2019-05-03 15:58:45 -05:00
Carson Sievert
e0eaa58779 update news 2019-05-03 15:57:55 -05:00
Carson Sievert
ced6622b25 Have getSliderType() return '' early if min, max, and value are NULL 2019-05-03 15:54:48 -05:00
Carson Sievert
2d2cf96f5e missed input_binding_slider.js 2019-05-03 15:48:47 -05:00
Carson Sievert
370f1b51ee Inputs now always supply a <label> tag with a special CSS class for hiding NULL labels
This helps to simplify the updating logic on the client
2019-05-03 15:38:57 -05:00
Winston Chang
67d3a504ae Merge pull request #2403 from rstudio/dateFormat
Consistent approach to coercing and formatting date strings
2019-05-03 10:58:06 -05:00
Carson Sievert
34ee48ef93 update news 2019-05-02 10:46:08 -05:00
Carson Sievert
c61a585e79 getSliderType() should be able to handle NULL min/max/value, fixes #2250 2019-05-01 19:41:26 -05:00
Carson Sievert
09388c9f07 Apply label updating logic all relevant input labels 2019-05-01 18:55:36 -05:00
Carson Sievert
b1bc78dad3 fix news link 2019-05-01 11:19:40 -05:00
Carson Sievert
a5a0f23c3a Use jQuery's text() method for proper escaping when inserting data.label string 2019-04-30 17:33:26 -05:00
Carson Sievert
4c50c064d3 make return value of dateYMD() slightly more clear 2019-04-30 17:08:13 -05:00
Carson Sievert
a63f271300 update news 2019-04-30 17:00:34 -05:00
Carson Sievert
08b22ff550 update NEWS 2019-04-30 17:00:34 -05:00
Carson Sievert
b04133bf65 Include argName with warning when length > 1 2019-04-30 17:00:34 -05:00
Carson Sievert
3602358d2c fix typo in warning message 2019-04-30 17:00:34 -05:00
Carson Sievert
67b0416eba Throw informative warning if date coercion fails and original input 2019-04-30 17:00:34 -05:00
Carson Sievert
f8d69ecb1f Consistent approach to coercing and formatting date strings, closes #2402 2019-04-30 17:00:34 -05:00
Carson Sievert
5e8bc204c1 make sure to remove label tag from DOM if label is updated to NULL 2019-04-26 19:08:15 -05:00
Carson Sievert
938332d646 Have textInput()'s receiveMessage method insert a label tag if one is needed, closes #868 2019-04-26 16:56:53 -05:00
Winston Chang
386078d441 Merge tag 'v1.3.2' 2019-04-23 14:07:37 -05:00
Winston Chang
4d778faaf4 Bump version to 1.3.2 2019-04-18 11:51:16 -05:00
Winston Chang
3055cf5602 Update NEWS 2019-04-18 11:49:09 -05:00
Joe Cheng
36373ba28b Merge pull request #2386 from rstudio/joe/bugfix/subapp-routing
Fix #2385: R Markdown documents containing subapps not rendering properly
2019-04-18 08:49:56 -07:00
Joe Cheng
1415b57181 Add sys.www.root to createAppHandlers, so that subapps can access /shared/* 2019-04-16 18:40:29 -07:00
Joe Cheng
65d4a4e906 Add comments 2019-04-16 18:12:09 -07:00
Joe Cheng
0abe221227 Use v1.3.1.9000 2019-04-14 17:21:26 -07:00
Joe Cheng
1b8d822226 Fix #2385: R Markdown documents containing subapps not rendering properly 2019-04-14 17:19:17 -07:00
Winston Chang
bc8fbd60d7 Bump version to 1.3.1.9000 2019-04-12 11:13:32 -05:00
Winston Chang
4c332eac9a Merge tag 'v1.3.1'
Shiny 1.3.1 on CRAN
2019-04-12 11:12:59 -05:00
Joe Cheng
f5392d77dc Merge pull request #2382 from rstudio/fix-index-html
Fix serving of www/index.html
2019-04-11 11:43:44 -07:00
Winston Chang
1e88990a0b Fix serving of www/index.html. Closes #2380 2019-04-11 11:57:48 -05:00
Joe Cheng
de4c7567d0 Manually bump the version numbers in shiny.js and shiny.min.js
Normally this would be where we grunt, but for this hotfix we
need to avoid the changes that went in at the end of 1.3.0 that
were accidentally left out of the built JS.
2019-04-10 11:42:42 -07:00
Joe Cheng
aff33dd023 Bump version to 1.3.1 2019-04-10 11:35:05 -07:00
Barret Schloerke
a287ebe324 Minimize str usage in rlog$valueStr (#2377)
* return early if loggin is disabled

* do not allow str to recurse

* add news item for #2377

* change "  " to " "

* Not a "world-ending performance issue"
2019-04-10 11:27:29 -07:00
Winston Chang
583a8d1001 Merge pull request #2353 from rstudio/fix-verbatim-text-wrap
Don't wrap text in verbatimTextOuput in Safari
2019-04-08 16:20:18 -05:00
Winston Chang
36a808add0 Update NEWS 2019-04-08 13:22:57 -05:00
Winston Chang
f651d4a274 Don't wrap text in verbatimTextOuput in Safari. Closes #2233 2019-04-08 13:20:20 -05:00
Winston Chang
f6e8e645f2 Bump version to 1.3.0.9000 2019-04-08 13:19:57 -05:00
Winston Chang
b4d2f88b74 Merge tag 'v1.3.0'
Shiny v1.3.0 on CRAN
2019-04-08 12:01:05 -05:00
Winston Chang
c524a736bd Re-document 2019-03-29 17:25:14 -05:00
Winston Chang
cdf3bf18f0 Fix broken URL 2019-03-29 16:55:41 -05:00
Winston Chang
b21bdacb4f Remove reactlog from Remotes 2019-03-27 13:42:25 -05:00
Winston Chang
92019b5ba3 Merge pull request #2361 from rstudio/fix-svg-foreignobject
Fix #2348, #2329, #1817: bugs triggered by networkD3 sankey plot
2019-03-27 13:40:15 -05:00
Alan Dipert
908d635063 Fix #2349, #2329, #1817: bugs triggered by networkD3 sankey plot
* All of these were caused by the presence of multiple body tags on the
page, which happened because networkD3's sankey plot generates SVGs
containing body tags via SVG's foreignObject tag
* In various places, the 'body' jQuery selector string is used under the
assumption there is only one 'body' tag on the page. The presence of
multiple 'body' tags breaks reliant code in strange ways.
* The fix was to use document.body or 'body:first' instead of 'body'.
2019-03-27 11:36:19 -07:00
Alan Dipert
20329feb7f Improve bootstrap-datepicker update tools, add docs 2019-03-26 20:33:42 -07:00
Alan Dipert
4cd92a1cd9 Add 'Fix datepicker DST bug' as patch
- Original commit: 0683b79
2019-03-26 20:33:32 -07:00
Alan Dipert
8ca3397c5d Improve bootstrap-datepicker update script 2019-03-26 20:33:20 -07:00
Alan Dipert
05cd79481e Re-import bootstrap-datepicker 1.6.4 2019-03-26 17:34:47 -07:00
Winston Chang
c0f1905785 Remove httpuv and reactlog from remotes 2019-03-26 15:18:00 -05:00
Alan Dipert
9afc06028d Restore intuitive bookmarking behavior (#2360)
* Adding flushPending() to ShinySession's flushOutput() restores intuitive bookmarking behavior

* Check that restoreContext is present

* Update NEWS
2019-03-26 15:08:34 -05:00
Barret Schloerke
7b6cc50238 Merge branch 'master' into rc-v1.3.0
* master:
  fix shortString is NA or NULL logic
  add coverage for situation where label might be na or NULL
  increase default length of label to 250chars from 100chars
  make sure labels are short for reactlog
2019-03-01 15:45:19 -05:00
Barret Schloerke
722b1d0258 Merge pull request #2345 from rstudio/short_reactlog_labels
Shorter reactlog labels
2019-03-01 14:43:59 -06:00
Barret Schloerke
93d3b78ac1 fix shortString is NA or NULL logic 2019-03-01 15:22:40 -05:00
Barret Schloerke
69e82f6e0e add coverage for situation where label might be na or NULL 2019-03-01 14:57:05 -05:00
Barret Schloerke
1f83a6db7b increase default length of label to 250chars from 100chars 2019-03-01 14:34:21 -05:00
Barret Schloerke
8f37951e14 make sure labels are short for reactlog 2019-03-01 14:30:23 -05:00
Joe Cheng
e1f4d43926 Merge pull request #2342 from rstudio/reactlog-cran
Reactlog github location removed
2019-02-27 10:31:08 -08:00
Joe Cheng
eb6139276f Merge pull request #2343 from rstudio/fix-resource-path
addResourcePath: create staticPath object immediately. Fixes #2339
2019-02-27 10:27:09 -08:00
Winston Chang
f18c426151 addResourcePath: create staticPath object immediately. Fixes #2339 2019-02-27 11:12:55 -06:00
Barret Schloerke
e46debb6d1 remove github location for reactlog and clean up flow of check_suggested 2019-02-27 09:19:46 -05:00
Barret Schloerke
d8b8739cb8 use httpuv rc-v1.5.0 branch 2019-02-26 16:57:28 -05:00
Barret Schloerke
9fd8eefa59 Merge branch 'master' into rc-v1.3.0
* master:
  Make sure the is.na() check in %AND% looks for length-1 input
2019-02-26 16:21:24 -05:00
Barret Schloerke
fd2af06a53 run grunt 2019-02-26 16:21:01 -05:00
Barret Schloerke
48f945ba7f use reactlog rc-1.0.0 branch 2019-02-26 16:19:24 -05:00
Barret Schloerke
6d59f88a76 bump news and description versions to 1.3.0 2019-02-26 16:19:07 -05:00
Joe Cheng
8b94d4626d Merge pull request #2338 from rstudio/fix-and
Make sure the is.na() check in %AND% looks for length-1 input
2019-02-26 13:11:39 -08:00
Winston Chang
d7d8e78e42 Make sure the is.na() check in %AND% looks for length-1 input
This is to avoid errors with R CMD check on R-devel like this:
https://travis-ci.org/rstudio/shiny/jobs/498880293
2019-02-26 14:32:41 -06:00
Joe Cheng
9755f86f53 Merge pull request #2327 from rstudio/staticpath-exclude
Exclude "session" from static path serving
2019-02-26 12:17:30 -08:00
Winston Chang
599a3ee82f Simplify session placement 2019-02-26 13:02:23 -06:00
Winston Chang
c790346490 Merge pull request #2284 from chasemc/patch-1
Fix typo
2019-02-21 14:04:40 -06:00
Joe Cheng
68cf3a9111 Merge pull request #2311 from rstudio/bookmark-dot
Bookmarking: restore inputs that have a leading dot
2019-02-21 11:56:40 -08:00
Barret Schloerke
59221dfcf2 bump dev version of reactlog. remove reactlog::reactlog_add_shiny_resource_paths() 2019-02-15 15:52:05 -06:00
Winston Chang
020413a206 Always exclude /session from static paths 2019-02-15 15:08:26 -06:00
Winston Chang
a343e9ebdf Use excludeStaticPath() function 2019-02-14 21:15:20 -06:00
Winston Chang
c304efee36 Exclude "session" from static path serving. Fixes #2325 2019-02-12 20:28:47 -06:00
Winston Chang
95173f676d Merge pull request #2319 from rstudio/joe/misc/constant-time-check
Add constant time check for shared secret
2019-02-11 15:22:54 -06:00
Joe Cheng
87d1db1f2b Fix test 2019-02-11 10:02:40 -08:00
Barret Schloerke
d445f384c7 Merge pull request #2315 from rstudio/reactlogShow
Add methods: reactlog, reactlogShow, and reactlogReset. Deprecate showReactLog
2019-02-07 15:05:05 -06:00
Joe Cheng
59dd4b0721 Code review feedback
- Rename sharedSecret variables to checkSharedSecret
- Don't perform the digest::digest(). This just means the timing could
  give away the length of the secret, but that's OK, there's enough
  entropy in the secret even if you know its length.
2019-02-05 14:33:04 -08:00
Joe Cheng
d73c91d4a7 Add unit tests for shared secret check 2019-02-04 14:19:02 -08:00
Joe Cheng
665a66522e Add constant time check for shared secret 2019-02-04 13:19:47 -08:00
Barret Schloerke
ba1efa65fa update man file name to reactlog from showReactLog in inst/staticdocs/index.r 2019-02-01 16:29:03 -05:00
Barret Schloerke
64a74692b9 document time for reactlogShow 2019-02-01 16:05:33 -05:00
Barret Schloerke
46cd285dd0 update docs by removing showReactLog/reactlogShow (to reactlog) as much as possible 2019-01-30 16:01:22 -05:00
Barret Schloerke
bcac115c3d Add methods: reactlog, reactlogShow, and reactlogReset. Depricate showReactLog
Update links to help file to not use `showReactLog`, but `reactlogShow`
Use updated reactlog pkg function api of reactlog_*. This may fail right now, but rerun travis when the reactlog code is merged into master.
2019-01-30 12:20:22 -05:00
Winston Chang
77ddb2c8c2 Bookmarking: restore inputs that have a leading dot. Fixes #2308 2019-01-23 12:05:24 -06:00
Barret Schloerke
8ae31eb998 Merge pull request #2107 from schloerke/barret/reactlog
Upgraded reactlog logging and support for shinyreactlog rendering
2019-01-11 13:19:14 -05:00
Barret Schloerke
7551a6ae1d add stats:: to setNames function calls
helps pass R CMD check
2019-01-11 13:04:39 -05:00
Barret Schloerke
93be659b1b merge Remotes 2019-01-11 12:45:59 -05:00
Barret Schloerke
3327878fc2 merged from master 2019-01-11 12:33:31 -05:00
Winston Chang
0b25c7f3c1 Merge pull request #2280 from rstudio/static-file
Use httpuv static file serving
2019-01-11 10:56:52 -06:00
Barret Schloerke
b606ba4dd7 added news item for reactlog 2019-01-11 09:43:25 -05:00
Chase Clark
0269bc810c Fix typo
"...use the JavaScript library selectize.js (https://github.com/selectize/selectize.js) ~~ to~~ instead of the basic select..."
2018-12-17 12:59:57 -06:00
Barret Schloerke
f2775f2c1d update rLog$msg output tests 2018-12-14 16:18:20 -05:00
Barret Schloerke
f06274aec6 fixed bad argument placement 2018-12-14 16:01:25 -05:00
Barret Schloerke
dfa686a3e0 always display the first n chars in a rLog$valueChange or rLog$define
capture the value in a try statement of capture.output of str
2018-12-14 15:51:31 -05:00
Barret Schloerke
fe679b5de5 add reactId to rLog$invalidateLater 2018-12-14 15:49:45 -05:00
Barret Schloerke
aa1eb0410c add force option to retrieving reactive info 2018-12-14 14:10:34 -05:00
Barret Schloerke
1b06bab7ee add define observer to rLog 2018-12-14 12:05:37 -05:00
Barret Schloerke
0f13056aa2 fix rLog$reset to work as an installed package. added a dummy context reactId (different from noReactId) 2018-12-14 12:05:22 -05:00
Barret Schloerke
beecf60db7 use rLog$reset() instead of initializeReactlog() due to changing global binding error 2018-12-13 17:04:12 -05:00
Barret Schloerke
160a2013bc fix broken test 2018-12-13 16:52:44 -05:00
Barret Schloerke
b8c636e87e move the actual setting of the reactiveValues key higher in set command for accurate logging 2018-12-13 16:52:35 -05:00
Barret Schloerke
add40e5926 when calling rlog$define, set a value 2018-12-13 16:51:32 -05:00
Barret Schloerke
960e7f3b24 fix .globals binding issue 2018-12-13 16:50:13 -05:00
Barret Schloerke
3e749f36e8 turn off logging of value in console 2018-12-13 16:50:03 -05:00
Barret Schloerke
8198d99309 add rlog$invalidateLater(runningCtxId, millis, domain) 2018-12-13 14:49:27 -05:00
Barret Schloerke
81de1c8ed4 remove setLabel from ReactiveValues 2018-12-13 14:42:25 -05:00
Barret Schloerke
3eb55e9d9b update reactiveValues set comments 2018-12-13 14:34:58 -05:00
Barret Schloerke
6b6ac86aea async start stop rLog should use domain = self 2018-12-13 14:33:29 -05:00
Barret Schloerke
1b45e70cbb use rLog$noReactId constant 2018-12-13 14:32:40 -05:00
Barret Schloerke
929f7ec235 document 2018-12-13 13:26:49 -05:00
Barret Schloerke
cf28d7e470 init testing for msg logging 2018-12-13 13:26:42 -05:00
Barret Schloerke
b0a00108f3 log, action, then perform invalidate action 2018-12-13 13:26:26 -05:00
Barret Schloerke
01151fc7f8 dummy context should be created every time. allow for id to be passed in 2018-12-13 13:26:12 -05:00
Barret Schloerke
bf8dbc38c7 add a noReactId label and init rLog method 2018-12-13 13:25:53 -05:00
Barret Schloerke
ae0d4d9353 add a default reactId for contexts for clearer msg logs and rLogs 2018-12-13 10:48:32 -05:00
Barret Schloerke
43ec4ae238 add helper functions for msg logger. 2018-12-13 10:48:02 -05:00
Barret Schloerke
c568a8cabe when updating a value for reactVal or a reactValues key, the context should not be recorded 2018-12-13 10:46:12 -05:00
Barret Schloerke
423bdd8b6b read reactlog version from description file 2018-12-12 11:27:17 -05:00
Barret Schloerke
1e19ff65e6 fix bad comma usage 2018-12-12 11:04:36 -05:00
Barret Schloerke
a9cf632f53 markTime -> userMark; queueEmpty -> idle 2018-12-12 10:58:28 -05:00
Barret Schloerke
fddf94a341 this check is already covered 2018-12-11 17:23:22 -05:00
Barret Schloerke
203168d261 dec/increment with integers 2018-12-11 17:23:13 -05:00
Barret Schloerke
0e3c3536f8 no need to store messages 2018-12-11 17:22:59 -05:00
Barret Schloerke
45b2b7e24f use curly brackets for all function defs 2018-12-11 17:22:33 -05:00
Barret Schloerke
88f177b065 use class brackets for R6 def 2018-12-11 17:22:09 -05:00
Barret Schloerke
ea7a8dd3ad consistent naming 2018-12-11 17:14:30 -05:00
Barret Schloerke
dda8f92494 remove writeReactLog 2018-12-11 17:13:09 -05:00
Barret Schloerke
26211802cd spelling and comments 2018-12-11 17:12:59 -05:00
Barret Schloerke
b4bef0d32c use reactlog::reactlog_add_shiny_resource_paths 2018-12-11 17:12:38 -05:00
Winston Chang
a8bf203067 Grunt 2018-12-10 14:34:27 -06:00
Winston Chang
624dd2e99d Bump version to 1.2.0.9001 2018-12-10 14:19:48 -06:00
Barret Schloerke
26a136a6e8 check_suggested now takes a github location and a source install script 2018-12-04 14:38:14 -05:00
Winston Chang
2d57ffa546 Update NEWS 2018-12-03 12:13:54 -06:00
Winston Chang
428b81a6d9 Use httpuv master branch 2018-12-03 12:10:04 -06:00
Barret Schloerke
f24c12fdfb shinyreactlog -> reactlog 2018-11-30 16:02:00 -05:00
Barret Schloerke
9a345d191b merge in master 2018-11-27 10:33:11 -05:00
Winston Chang
fec706d134 Add headers for static serving 2018-11-20 12:25:46 -06:00
Winston Chang
c338448997 Use shiny-shared-secret validation for static files 2018-11-20 12:25:46 -06:00
Winston Chang
956c1cb1a7 Use setStaticPath instead of setStaticPaths 2018-11-20 12:25:46 -06:00
Winston Chang
8831b4da9e Use static serving for app's own assets 2018-11-20 12:25:46 -06:00
Winston Chang
f8bd60dcd7 Use httpuv static serving 2018-11-20 12:25:46 -06:00
Winston Chang
6a373b585c Merge pull request #2248 from rstudio/fix-selectize-label
Make updateSelectizeInput() work with labels again
2018-11-15 17:04:16 -06:00
Winston Chang
54480e2510 Merge branch 'master' into fix-selectize-label 2018-11-15 17:03:58 -06:00
Joe Cheng
83f73603db Merge pull request #2257 from colearendt/fix-htmltools-dep
fix dependency version since htmltools 0.3.6 is used
2018-11-15 14:39:37 -08:00
Joe Cheng
2b10f192ba Merge pull request #2261 from rstudio/joe/bugfix/async-rendercachedplot
Fix #2247: Async cached plots raise "Error in !: invalid argument type" error
2018-11-15 14:39:07 -08:00
Winston Chang
775d5289cb Grunt 2018-11-15 15:23:35 -06:00
Winston Chang
e6c66352a7 Update NEWS 2018-11-15 15:23:35 -06:00
Winston Chang
77afd73ee1 Use new selectize suffix. Fixes #2245 2018-11-15 15:23:35 -06:00
Winston Chang
5ac96a40aa Remove QuitChildProcessesOnExit: Default option 2018-11-15 15:10:54 -06:00
Winston Chang
2fea0e2598 Don't byte-compile when doing local install in RStudio 2018-11-15 15:08:49 -06:00
Joe Cheng
2b64949cbe Fix #2247: Async cached plots raise "Error in !: invalid argument type" error 2018-11-14 16:45:40 -08:00
Cole Arendt
918d57f25e fix dependency version since htmltools 0.3.6 is used 2018-11-11 15:04:44 -05:00
Joe Cheng
5e2b40d3a9 Bump version for development 2018-11-02 13:11:04 -07:00
Joe Cheng
979ef4bd43 Merge remote-tracking branch 'origin/v1.2-rc' 2018-11-02 13:10:48 -07:00
Winston Chang
914baf594b Merge pull request #2241 from rstudio/joe/bugfix/icon-examples
Remove icon examples
2018-11-01 20:41:58 -05:00
Joe Cheng
02b0802886 Add note about FontAwesome path change 2018-11-01 15:08:59 -07:00
Joe Cheng
0725239397 Remove icon examples
These cause browser windows to pop up during R CMD check, which is
against CRAN policy. @wch will merge a PR that has other examples
once we release v1.2.
2018-11-01 14:56:15 -07:00
Joe Cheng
d72e8a06a7 Fix error in global reactiveTimer
When reactiveTimer is created without a default reactive domain
(i.e. outside of a session, i.e. global) there's no session to
call cycleStartAction on. Instead, invalidation should proceed
right away.

Fixes #2228
2018-10-29 11:43:03 -05:00
Joe Cheng
cf79fec720 Merge pull request #2226 from rstudio/joe/bugfix/cycle-queue-stall-2
Fix input event queue stall
2018-10-25 15:19:23 -07:00
Joe Cheng
31dda45d1c Update NEWS 2018-10-25 12:13:37 -07:00
Joe Cheng
9836b72661 Fix #2225: Input event queue can stall in apps that use async 2018-10-25 12:12:21 -07:00
Winston Chang
6ede0194c6 Update license information in README 2018-10-25 12:15:57 -05:00
Winston Chang
5ec38581ca Add support for Font-Awesome 5 brands (#2221)
* Add support for Font-Awesome 5 brands

* Fix glyphicon support
2018-10-24 16:13:36 -05:00
Winston Chang
2629e59ace Re-document 2018-10-18 22:52:43 -05:00
Alan Dipert
f3eb770e20 Add to fontawesome news entry (#2214)
* Add FontAwesome upgrade information to NEWS.md

* Update NEWS.md

* Update NEWS.md
2018-10-18 11:57:29 -05:00
Winston Chang
0683b79fac Fix datepicker DST bug (#2212)
* Fix datepicker DST bug. Closes #2204

This fix is borrowed from:
13885397de (diff-dd513a8bab7ad1033c8784c4a1b9ce15)

* Update NEWS.md
2018-10-17 15:01:39 -05:00
Barret Schloerke
9963ba6cf5 merge master 2018-09-18 12:26:57 -04:00
Barret Schloerke
21ff005c1a remove display param from MessageLogger 2018-09-18 10:59:31 -04:00
Barret Schloerke
206b9135f1 if reactlog console option is set, display, or display is display is true 2018-09-18 10:24:48 -04:00
Barret Schloerke
5449de1a67 use shinyreactlog pkg directly 2018-09-18 10:24:17 -04:00
Barret Schloerke
47c61756e6 log create context with srcref and srcfile 2018-09-18 10:24:01 -04:00
Barret Schloerke
f28900f8ca merged master 2018-09-10 12:50:42 -04:00
Barret Schloerke
e0c15c42d7 do not depend on null reactid values 2018-09-05 10:40:45 -04:00
Barret Schloerke
facef1d23c do not set shiny.reactlog option by default 2018-07-09 22:23:28 -04:00
Barret Schloerke
cdb446375c turn all active isLogging bindings into functions 2018-07-09 22:22:49 -04:00
Barret Schloerke
6f7b2887aa fix parameters for shinyreactlog (session_token) 2018-07-06 11:58:37 -04:00
Barret Schloerke
bc8ae063dd add new option for shinyreactlog messages in the console 2018-07-06 11:58:16 -04:00
Barret Schloerke
003dc39d76 add shinyreactlog as remote 2018-06-22 10:49:57 -04:00
Barret Schloerke
0b04c28011 move renderReactLog calculation above addResourcePath 2018-06-21 16:54:23 -04:00
Barret Schloerke
31854ad9e8 add reactlog resource path when calling for reactlog 2018-06-21 15:24:17 -04:00
Barret Schloerke
4304e92f0d use self$ for all fn calls within rLog to avoid any conflicts 2018-06-21 15:23:33 -04:00
Barret Schloerke
44736cefbf allow for null context id in dependents 2018-06-21 15:22:46 -04:00
Barret Schloerke
a807449f28 remove old temp files 2018-06-21 15:22:16 -04:00
Barret Schloerke
ae9d38b59c remove old .graph methods and use shinyreactlog pkg for rendering 2018-06-21 10:31:12 -04:00
Barret Schloerke
05e50c1b98 use original yarn lock file 2018-06-20 15:54:07 -04:00
Barret Schloerke
e11004da7b remove _ignore folder 2018-06-20 15:51:17 -04:00
Barret Schloerke
97ee7b5d96 clean up tools readme to use yarn over global grunt-cli install 2018-06-20 15:47:16 -04:00
Barret Schloerke
6c6e2573aa remove a LOT of files in favor of github.com/schloerke/shinyreactlog
still need hooks to shinyreactlog pkg
2018-06-20 15:34:22 -04:00
Barret Schloerke
8992827f21 merged master 2018-06-20 14:11:22 -04:00
Barret Schloerke
893b9c1b38 merged master -> barret/reactlog 2018-06-19 09:24:39 -04:00
Barret Schloerke
6f8166ca0f add todo 2018-06-14 16:06:12 -05:00
Barret Schloerke
64db035d77 simplify colors
no active green or grey. input invalidate should be grey
2018-06-08 15:38:03 -04:00
Barret Schloerke
cb051e4254 less case "STRING" and more case OBJ.VALUE 2018-06-08 15:20:51 -04:00
Barret Schloerke
20e9c2901d reduced hoverStatusOnNodeIds arg requirements and internals 2018-06-08 15:17:33 -04:00
Barret Schloerke
ce4b391495 add more flow classes and remove $FlowExpectError 2018-06-08 15:16:43 -04:00
Barret Schloerke
7d932f5b18 fix searching on nodes 2018-06-08 11:43:37 -04:00
Barret Schloerke
56c8c08e08 move mapValues to a util file 2018-06-08 11:42:37 -04:00
Barret Schloerke
13ef25c0b5 bump flow threshold to 95 percent 2018-06-08 11:41:10 -04:00
Barret Schloerke
6abfa5bf80 default log with marks 2018-06-06 16:14:54 -04:00
Barret Schloerke
20ae8e4f8b fix eslint and prettier clashes 2018-06-06 16:14:51 -04:00
Barret Schloerke
f595c5d504 first pass at a user time mark 2018-06-06 16:14:47 -04:00
Barret Schloerke
972779253c update enter exit to not be off by one 2018-06-06 16:14:42 -04:00
Barret Schloerke
9179a241e9 first pass at reactlog mark 2018-06-06 16:14:31 -04:00
Barret Schloerke
85e7e89ad9 fix babel options bug in grunt config 2018-06-06 16:14:26 -04:00
Barret Schloerke
9f5bc00c89 add simpler lint then watch script 2018-05-31 14:00:55 -04:00
Barret Schloerke
0ab842e3c5 add cranwhales log 2018-05-31 14:00:32 -04:00
Barret Schloerke
3a0a3e49dc use log states from dictionary, rather than copies 2018-05-31 13:58:45 -04:00
Barret Schloerke
438b1c043e set app data as log 2018-05-31 13:57:56 -04:00
Barret Schloerke
6d13b65e7c export rlog object not as default 2018-05-31 13:57:27 -04:00
Barret Schloerke
423d41ee0e fix console bug 2018-05-31 13:55:59 -04:00
Barret Schloerke
1b61d9bc51 first pass at freeze/thaw in rlog 2018-05-31 13:55:32 -04:00
Barret Schloerke
bf0c3d42db copy all rlog files to the temp directory 2018-05-31 13:53:58 -04:00
Barret Schloerke
5394a68314 attempt to load rlog_data as a trycatch to work with showReactLog() 2018-05-31 13:53:46 -04:00
Barret Schloerke
b0063399bb fix freeze/thaw rlog'ing 2018-05-30 14:14:57 -04:00
Barret Schloerke
724c6b7656 Merge branch 'master' into barret/reactlog
* master:
  runApp: add support for IPv6 addresses
  Bump version to *.9000. Back to work!
  Bump version to 1.1.0
  Bump httpuv version and add NEWS note
  Fix #2061: Tests failing on Windows due to rounding errors
  Take dependency on later >=0.7.2
2018-05-29 18:26:24 -04:00
Barret Schloerke
0530cbcd0f make sure log works with an empty log 2018-05-29 18:22:26 -04:00
Barret Schloerke
6e2bba1513 up the flow type percentage 2018-05-29 18:16:40 -04:00
Barret Schloerke
89ac5d7c42 ignore yarn error 2018-05-29 18:16:08 -04:00
Barret Schloerke
dd68722b66 first pass at cyto flow types 2018-05-29 12:01:36 -04:00
Barret Schloerke
933d5db2ab flow more files 2018-05-29 09:59:19 -04:00
Barret Schloerke
0386ed6409 add index file for updateGraph to gather all exports for easy import 2018-05-29 09:34:06 -04:00
Barret Schloerke
d3c14bf416 first pass at flowtype 2018-05-29 09:33:02 -04:00
Barret Schloerke
2a224ce9fb add babel plugin transform class properties within yarn 2018-05-29 09:32:53 -04:00
Barret Schloerke
78322525b7 add flow, flow scripts and update grunt-babel 2018-05-29 09:25:39 -04:00
Barret Schloerke
5b7c9c205e remove rlog grunt tasks in favor of config files 2018-05-29 09:24:44 -04:00
Barret Schloerke
07ac70a460 add lodash flow types 2018-05-29 09:23:30 -04:00
Barret Schloerke
3629f806a2 add jquery flow types 2018-05-29 09:23:21 -04:00
Barret Schloerke
72fc43c738 add a flow config
only for rlog src
make all lints warnings
any suppress comment starts with "\\ $Flow"
and if strict (currently none) do recommended strict things
2018-05-29 09:23:06 -04:00
Barret Schloerke
df38f0be3f clean up lint config 2018-05-23 17:02:26 -04:00
Barret Schloerke
808684c2a8 remove unused dep and script 2018-05-23 17:00:57 -04:00
Barret Schloerke
69ed3a7751 working graph with es6 modules 2018-05-23 16:40:24 -04:00
Barret Schloerke
68556caa9a first pass at distributed files. graph loads, not all perfect 2018-05-23 12:02:59 -04:00
Barret Schloerke
bb8ea8053b prettier and build script updates 2018-05-22 11:44:49 -04:00
Barret Schloerke
6f01e6edf1 first pass at sep classes 2018-05-22 10:38:10 -04:00
Barret Schloerke
66a74d16ff lints 2018-05-22 10:01:07 -04:00
Barret Schloerke
0e525f5aeb add Console module 2018-05-22 09:59:52 -04:00
Barret Schloerke
86007c466d copy in react_graph into index.js to start pruning into multiple files 2018-05-17 16:56:21 -04:00
Barret Schloerke
7b39b79183 added prettier config for rlog 2018-05-17 16:42:02 -04:00
Barret Schloerke
7f453aa6f6 add local rlog .eslintrc.js 2018-05-17 16:38:59 -04:00
Barret Schloerke
f36052ffeb add test files 2018-05-17 16:38:34 -04:00
Barret Schloerke
d35db11f43 add gitignore in rlog 2018-05-17 16:19:20 -04:00
Barret Schloerke
173e5d3f97 prettier and lints 2018-05-17 16:18:05 -04:00
Barret Schloerke
bcebf737c3 move node_modules and grunt file to root dir 2018-05-17 16:13:13 -04:00
Barret Schloerke
5280b72b85 add different log files for rlog to check 2018-05-15 12:21:29 -04:00
Barret Schloerke
a4dfe7138e search regex implemented to update filtered data on getGraph.atStep(k) 2018-05-14 16:01:04 -04:00
Barret Schloerke
b9960bad1a next, prev, next cycle, prev cycle implemented within new search 2018-05-14 15:34:35 -04:00
Barret Schloerke
e1d7805396 massive sweep on how filtering and hovering is done. Commiting. regex filter is unfinished 2018-05-11 14:59:50 -04:00
Barret Schloerke
ce6f993f0e add filter by name 2018-05-10 10:54:14 -04:00
Barret Schloerke
aa1d94e6c9 first pass double click filter 2018-05-09 10:42:45 -04:00
Barret Schloerke
00a6092836 remove TODO 2018-05-09 10:41:45 -04:00
Barret Schloerke
f6372faa23 for future... make animation a setting 2018-05-04 11:10:01 -04:00
Barret Schloerke
1a5e266d26 Drastically improve performance by not re-rendering the layout on a layout that isn't changing 2018-05-04 11:02:33 -04:00
Barret Schloerke
2e4a107201 fix hover and sticky hover to be stable throughout transitions
all items use `hoverKey`. edges are supplying their ghostKey so that all edges share the same hoverKey.  if A --> B then all edges from A to B will behave the same way.
2018-05-04 11:01:31 -04:00
Barret Schloerke
2559496ded first pass at hover highlight. need to move to graph data object and not cyto object 2018-05-03 13:27:20 -04:00
Barret Schloerke
d3aa82fc5d clean up graph addEntry wrapper 2018-05-03 13:26:53 -04:00
Barret Schloerke
704605918d update layout options 2018-05-03 13:26:35 -04:00
Barret Schloerke
7e8116888b add alt shift arrows navigation and prev / next step calculations 2018-05-03 13:25:25 -04:00
Barret Schloerke
e0f4bbd20d skip adding entries if the reactId is rNoCtx 2018-05-02 15:12:21 -04:00
Barret Schloerke
5ae2d5a24b Allow for isolate calls to have no context and input name changes to have no context 2018-05-02 15:11:58 -04:00
Barret Schloerke
8648737a7a fix missing period bug 2018-05-02 15:11:19 -04:00
Barret Schloerke
6e090d5112 active enter and value change now pulse and use ActiveStateStatus helper 2018-05-02 11:25:08 -04:00
Barret Schloerke
2207e561f2 fix progress bar tick leaking right bug 2018-05-02 11:24:33 -04:00
Barret Schloerke
b9cd5b572b first pass at ActiveStateStatus class with invalidate 2018-05-02 11:04:30 -04:00
Barret Schloerke
344c6f3ee7 use graph style and do not animate graph style 2018-05-02 11:03:52 -04:00
Barret Schloerke
f6f2c0ed56 first pass at cacheing graphs. wait for now 2018-05-02 11:01:01 -04:00
Barret Schloerke
ec7a66a966 make edges shades of grey 2018-05-02 11:00:34 -04:00
Barret Schloerke
23ca428a01 add cycle markers in the timeline 2018-05-02 10:58:47 -04:00
Barret Schloerke
eb9f251e34 add nav buttons 2018-05-02 10:58:15 -04:00
Barret Schloerke
394d875eb4 valuechange addressed when an isolateInvalidateEnd is called
invalidate end also sets color to a 'done' grey
2018-05-01 14:12:02 -04:00
Barret Schloerke
4cc6403867 do not double log observable set invalidation 2018-05-01 14:09:09 -04:00
Barret Schloerke
9d5fa773f3 add classes and colors for different states of a graph
* reactive key value change until invalidate end has finished
* latest enter is darker green than others
* mousedown added to timeline click
* mousedown and mouse movement added to timeline
2018-05-01 10:08:37 -04:00
Barret Schloerke
075ca49a1f log that invalidation has occured when an input value changes a key 2018-05-01 10:06:13 -04:00
Barret Schloerke
9564f1d871 invalidate rlog namesDeps on value change 2018-05-01 10:05:36 -04:00
Barret Schloerke
cf546a47b6 on rlog object definition, do not trigger a value change 2018-05-01 10:05:23 -04:00
Barret Schloerke
d3a4f35170 merge master --> reactlog 2018-04-30 11:25:03 -04:00
Barret Schloerke
f450aea449 allow for skipping to next cycle by holding altKey and arrow L/R 2018-04-30 11:21:15 -04:00
Barret Schloerke
aed308b259 styles added to animation in cyto nodes 2018-04-30 11:20:45 -04:00
Barret Schloerke
714dffc943 set up ghost edge and use classes in cyto graph 2018-04-30 11:04:13 -04:00
Barret Schloerke
f8a173efbd first pass at cytoscape.js graph 2018-04-30 10:06:59 -04:00
Barret Schloerke
70e7822dd1 be clear in action name provided in log and give dependsOnRemove a ctxId 2018-04-30 10:06:40 -04:00
Barret Schloerke
452631550a single quotes to double quotes 2018-04-24 11:34:12 -04:00
Barret Schloerke
a14266b452 add freeze and thaw to logger 2018-04-24 11:34:00 -04:00
Barret Schloerke
ceb19c7573 use an rLog object to do all logging 2018-04-24 10:49:16 -04:00
Barret Schloerke
7336d327b3 first pass at adding domain to all rlog functions 2018-04-18 11:49:11 -04:00
Barret Schloerke
c9c5225a6a add rlogAsyncStart and rlogAsyncStop 2018-04-17 10:58:20 -04:00
Barret Schloerke
e1060bf537 isolate calls should be handled differently than regular rlog calls 2018-04-17 10:10:01 -04:00
Barret Schloerke
392e42a55d clean up when reactivevalues are defined and updated in rlog 2018-04-17 10:09:34 -04:00
Barret Schloerke
b974e41148 add test app for rlog 2018-04-17 10:09:00 -04:00
Barret Schloerke
aa3e2a0b64 ctxId's are now upgraded to start with 'ctx' in logging 2018-04-17 09:47:26 -04:00
Barret Schloerke
3df89dd9a3 local logging done with ". " for spacing 2018-04-17 09:46:54 -04:00
Barret Schloerke
6ef751422a first pass at reformatting rlog 2018-04-16 13:37:50 -04:00
Barret Schloerke
05d49ee45e use MessageLogger for node information cache 2018-04-16 09:47:58 -04:00
Barret Schloerke
3e4783c454 remove dot syntax 2018-04-16 09:33:09 -04:00
Barret Schloerke
ce93201843 make the rlog messages a r6 object 2018-04-16 09:28:54 -04:00
Barret Schloerke
f9fc3a46b5 change all nodeId to reactId 2018-04-16 09:17:04 -04:00
Barret Schloerke
0467d6666a merge master -> barret/reactlog 2018-04-13 11:26:34 -04:00
Barret Schloerke
1f26b076a3 first pass gantt chart... brings up future thoughts
could add a gantt chart at bottom of react-graph for the current execution session. Would be interesting to have a full gantt of the current execution 'cycle' with a bar indicating where we are in time to give context to the current graph layout. the gantt coult reset at each 'cycle' as the context is reset as well
2018-04-13 11:22:23 -04:00
Barret Schloerke
7944f21925 break apart the large react-graph.html file 2018-04-13 10:20:08 -04:00
Barret Schloerke
e91eda8eca add npm scripts to build, clean, and watch the js 2018-04-13 10:19:10 -04:00
Barret Schloerke
d8ac84a5da add rLogValueChange (no start/end, just change) 2018-04-13 10:18:11 -04:00
Barret Schloerke
3098a02b72 first pass at making rlog. need javascript to recognize new log format 2018-04-13 10:07:03 -04:00
78 changed files with 2097 additions and 2299 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@
shinyapps/
README.html
.*.Rnb.cached
tools/yarn-error.log

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 1.2.0
Version: 1.3.2.9000
Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
@@ -65,28 +65,32 @@ Depends:
Imports:
utils,
grDevices,
httpuv (>= 1.4.4),
httpuv (>= 1.5.0),
mime (>= 0.3),
jsonlite (>= 0.9.16),
xtable,
digest,
htmltools (>= 0.3.5),
htmltools (>= 0.3.6),
R6 (>= 2.0),
sourcetools,
later (>= 0.7.2),
promises (>= 1.0.1),
tools,
crayon,
rlang
rlang,
fastmap
Suggests:
datasets,
Cairo (>= 1.5-5),
testthat,
testthat (>= 2.2.1),
knitr (>= 1.6),
markdown,
rmarkdown,
ggplot2,
reactlog (>= 1.0.0),
magrittr
Remotes:
wch/fastmap
URL: http://shiny.rstudio.com
BugReports: https://github.com/rstudio/shiny/issues
Collate:
@@ -106,6 +110,7 @@ Collate:
'cache-utils.R'
'diagnose.R'
'fileupload.R'
'font-awesome.R'
'graph.R'
'reactives.R'
'reactive-domains.R'
@@ -159,4 +164,5 @@ Collate:
'test-export.R'
'timer.R'
'update-input.R'
RoxygenNote: 6.1.0
RoxygenNote: 6.1.1
Encoding: UTF-8

View File

@@ -189,6 +189,9 @@ export(reactiveUI)
export(reactiveVal)
export(reactiveValues)
export(reactiveValuesToList)
export(reactlog)
export(reactlogReset)
export(reactlogShow)
export(registerInputHandler)
export(removeInputHandler)
export(removeModal)
@@ -298,5 +301,6 @@ import(httpuv)
import(methods)
import(mime)
import(xtable)
importFrom(fastmap,fastmap)
importFrom(grDevices,dev.cur)
importFrom(grDevices,dev.set)

86
NEWS.md
View File

@@ -1,3 +1,75 @@
shiny 1.3.2.9000
=======
### Improvements
* Resolved ([#2402](https://github.com/rstudio/shiny/issues/2402)): An informative warning is now thrown for mis-specified (date) strings in `dateInput()`, `updateDateInput()`, `dateRangeInput()`, and `updateDateRangeInput()`. ([#2403](https://github.com/rstudio/shiny/pull/2403))
### Bug fixes
* Fixed [#2387](https://github.com/rstudio/shiny/issues/2387): Updating a `sliderInput()`'s type from numeric to date no longer changes the rate policy from debounced to immediate. More generally, updating an input binding with a new type should (no longer) incorrectly alter the input rate policy. ([#2404](https://github.com/rstudio/shiny/pull/2404))
* Fixed [#868](https://github.com/rstudio/shiny/issues/868): If an input is initialized with a `NULL` label, it can now be updated with a string. Moreover, if an input label is initialized with a string, it can now be removed by updating with `label=character(0)` (similar to how `choices` and `selected` can be cleared in `updateSelectInput()`). ([#2406](https://github.com/rstudio/shiny/pull/2406))
* Fixed [#2250](https://github.com/rstudio/shiny/issues/2250): `updateSliderInput()` now works with un-specified (or zero-length) `min`, `max`, and `value`. ([#2416](https://github.com/rstudio/shiny/pull/2416))
* Fixed [#2233](https://github.com/rstudio/shiny/issues/2233): `verbatimTextOutput()` produced wrapped text on Safari, but the text should not be wrapped. ([#2353](https://github.com/rstudio/shiny/pull/2353))
* Fixed [rstudio/reactlog#36](https://github.com/rstudio/reactlog/issues/36): Changes to reactive values not displaying accurately in reactlog. ([#2424](https://github.com/rstudio/shiny/pull/2424))
* Fixed [#2329](https://github.com/rstudio/shiny/issues/2329), [#1817](https://github.com/rstudio/shiny/issues/1817): These bugs were reported as fixed in Shiny 1.3.0 but were not actually fixed because some JavaScript changes were accidentally not included in the release. The fix resolves issues that occur when `withProgressBar()` or bookmarking are combined with the [networkD3](https://christophergandrud.github.io/networkD3/) package's Sankey plot.
shiny 1.3.2
===========
### Bug fixes
* Fixed [#2285](https://github.com/rstudio/shiny/issues/2285), [#2288](https://github.com/rstudio/shiny/issues/2288): Static CSS/JS resources in subapps in R Markdown documents did not render properly. ([#2386](https://github.com/rstudio/shiny/pull/2386))
* Fixed [#2280](https://github.com/rstudio/shiny/issues/2280): Shiny applications that used a www/index.html file did not serve up the index file. ([#2382](https://github.com/rstudio/shiny/pull/2382))
shiny 1.3.1
===========
## Full changelog
### Bug fixes
* Fixed a performance issue introduced in v1.3.0 when using large nested lists within Shiny. ([#2377](https://github.com/rstudio/shiny/pull/2377))
shiny 1.3.0
===========
## Full changelog
### Breaking changes
### New features
* Revamped Shiny's [reactlog](https://github.com/rstudio/reactlog) viewer which debugs reactivity within a shiny application. This allows users to traverse the reactivity history of a shiny application, filter to the dependency tree of a selected reactive object, and search for matching reactive objects. See `?reactlogShow` for more details and how to enable this feature. ([#2107](https://github.com/rstudio/shiny/pull/2107))
* Shiny now serves static files on a background thread. This means that things like JavaScript and CSS assets can be served without blocking or being blocked by the main R thread, and should result in significantly better performance for heavily loaded servers. ([#2280](https://github.com/rstudio/shiny/pull/2280))
### Minor new features and improvements
* The `Shiny-Shared-Secret` security header is now checked using constant-time comparison to prevent timing attacks (thanks @dirkschumacher!). ([#2319](https://github.com/rstudio/shiny/pull/2319))
### Bug fixes
* Fixed [#2245](https://github.com/rstudio/shiny/issues/2245): `updateSelectizeInput()` did not update labels. ([#2248](https://github.com/rstudio/shiny/pull/2248))
* Fixed [#2308](https://github.com/rstudio/shiny/issues/2308): When restoring a bookmarked application, inputs with a leading `.` would not be restored. ([#2311](https://github.com/rstudio/shiny/pull/2311))
* Fixed [#2305](https://github.com/rstudio/shiny/issues/2305), [#2322](https://github.com/rstudio/shiny/issues/2322), [#2351](https://github.com/rstudio/shiny/issues/2351): When an input in dynamic UI is restored from bookmarks, it would keep getting set to the same value. ([#2360](https://github.com/rstudio/shiny/pull/2360))
* Fixed [#2349](https://github.com/rstudio/shiny/issues/2349), [#2329](https://github.com/rstudio/shiny/issues/2329), [#1817](https://github.com/rstudio/shiny/issues/1817): These were various bugs triggered by the presence of the [networkD3](https://christophergandrud.github.io/networkD3/) package's Sankey plot in an app. Impacted features included `dateRangeInput`, `withProgressBar`, and bookmarking ([#2359](https://github.com/rstudio/shiny/pull/2359))
### Documentation Updates
* Fixed [#2247](https://github.com/rstudio/shiny/issues/2247): `renderCachedPlot` now supports using promises for either `expr` or `cacheKeyExpr`. (Shiny v1.2.0 supported async `expr`, but only if `cacheKeyExpr` was async as well; now you can use any combination of sync/async for `expr` and `cacheKeyExpr`.) [#2261](https://github.com/rstudio/shiny/pull/2261)
shiny 1.2.0
===========
@@ -5,13 +77,17 @@ This release features plot caching, an important new tool for improving performa
## Full changelog
### Breaking changes
* The URL paths for FontAwesome CSS/JS/font assets have changed, due to our upgrade from FontAwesome 4 to 5. This shouldn't affect you unless you're using `www/index.html` to provide your UI and have hardcoded the old FontAwesome paths into your HTML. If that's you, consider switching to [HTML templates](https://shiny.rstudio.com/articles/templates.html), which give you the syntax of raw HTML while still taking advantage of Shiny's automatic management of web dependencies.
### 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).
* Upgrade FontAwesome from 4.7.0 to 5.3.1 and made `icon` tags browsable, which means they will display in a web browser or RStudio viewer by default ([#2186](https://github.com/rstudio/shiny/issues/2186)). Note that if your application or library depends on FontAwesome directly using custom CSS, you may need to make some or all of the changes recommended in [Upgrade from Version 4](https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4). Font Awesome icons can also now be used in static R Markdown documents.
* 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))
@@ -47,6 +123,12 @@ This release features plot caching, an important new tool for improving performa
* 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. (Thanks @GaGaMan1101!) [#2212](https://github.com/rstudio/shiny/pull/2212)
* Fixed [#2225](https://github.com/rstudio/shiny/issues/2225): Input event queue can stall in apps that use async. [#2226](https://github.com/rstudio/shiny/pull/2226)
* Fixed [#2228](https://github.com/rstudio/shiny/issues/2228): `reactiveTimer` fails when not owned by a session. Thanks, @P-Bettega! [#2229](https://github.com/rstudio/shiny/pull/2229)
### 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))
@@ -107,7 +189,7 @@ This is a significant release for Shiny, with a major new feature that was nearl
* 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))
* Changed script tags in reactlog ([inst/www/reactive-graph.html](https://github.com/rstudio/shiny/blob/v1.1.0/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.

40
R/app.R
View File

@@ -170,7 +170,14 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
}
wwwDir <- file.path.ci(appDir, "www")
if (dirExists(wwwDir)) {
staticPaths <- list("/" = staticPath(wwwDir, indexhtml = FALSE, fallthrough = TRUE))
} else {
staticPaths <- list()
}
fallbackWWWDir <- system.file("www-dir", package = "shiny")
serverSource <- cachedFuncWithFile(appDir, "server.R", case.sensitive = FALSE,
function(serverR) {
# If server.R contains a call to shinyServer (which sets .globals$server),
@@ -220,6 +227,13 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
structure(
list(
staticPaths = staticPaths,
# Even though the wwwDir is handled as a static path, we need to include
# it here to be handled by R as well. This is because the special case
# of index.html: it is specifically not handled as a staticPath for
# reasons explained above, but if someone does want to serve up an
# index.html, we need to handle it, and we do it by using the
# staticHandler in the R code path. (#2380)
httpHandler = joinHandlers(c(uiHandler, wwwDir, fallbackWWWDir)),
serverFuncSource = serverFuncSource,
onStart = onStart,
@@ -309,6 +323,20 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
}
wwwDir <- file.path.ci(appDir, "www")
if (dirExists(wwwDir)) {
# wwwDir is a static path served by httpuv. It does _not_ serve up
# index.html, for two reasons. (1) It's possible that the user's
# www/index.html file is not actually used as the index, but as a template
# that gets processed before being sent; and (2) the index content may be
# modified by the hosting environment (as in SockJSAdapter.R).
#
# The call to staticPath normalizes the path, so that if the working dir
# later changes, it will continue to point to the right place.
staticPaths <- list("/" = staticPath(wwwDir, indexhtml = FALSE, fallthrough = TRUE))
} else {
staticPaths <- list()
}
fallbackWWWDir <- system.file("www-dir", package = "shiny")
oldwd <- NULL
@@ -327,6 +355,18 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
structure(
list(
# fallbackWWWDir is _not_ listed in staticPaths, because it needs to
# come after the uiHandler. It also does not need to be fast, since it
# should rarely be hit. The order is wwwDir (in staticPaths), then
# uiHandler, then falbackWWWDir (which is served up by the R
# staticHandler function).
staticPaths = staticPaths,
# Even though the wwwDir is handled as a static path, we need to include
# it here to be handled by R as well. This is because the special case
# of index.html: it is specifically not handled as a staticPath for
# reasons explained above, but if someone does want to serve up an
# index.html, we need to handle it, and we do it by using the
# staticHandler in the R code path. (#2380)
httpHandler = joinHandlers(c(dynHttpHandler, wwwDir, fallbackWWWDir)),
serverFuncSource = dynServerFuncSource,
onStart = onStart,

View File

@@ -426,7 +426,7 @@ RestoreInputSet <- R6Class("RestoreInputSet",
},
asList = function() {
as.list.environment(private$values)
as.list.environment(private$values, all.names = TRUE)
}
)
)

View File

@@ -1508,10 +1508,6 @@ downloadLink <- function(outputId, label="Download", class=NULL, ...) {
#'
#'
#' @examples
#' icon("calendar") # standard icon
#' icon("calendar", "fa-3x") # 3x normal size
#' icon("cog", lib = "glyphicon") # From glyphicon library
#'
#' # add an icon to a submit button
#' submitButton("Update View", icon = icon("refresh"))
#'
@@ -1537,8 +1533,13 @@ icon <- function(name, class = NULL, lib = "font-awesome") {
# build the icon class (allow name to be null so that other functions
# e.g. buildTabset can pass an explicit class value)
iconClass <- ""
if (!is.null(name))
iconClass <- paste0(prefix, " ", prefix, "-", name)
if (!is.null(name)) {
prefix_class <- prefix
if (prefix_class == "fa" && name %in% font_awesome_brands) {
prefix_class <- "fab"
}
iconClass <- paste0(prefix_class, " ", prefix, "-", name)
}
if (!is.null(class))
iconClass <- paste(iconClass, class)

View File

@@ -179,7 +179,7 @@ MemoryCache <- R6Class("MemoryCache",
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$cache <- fastmap()
private$max_size <- max_size
private$max_age <- max_age
private$max_n <- max_n
@@ -208,7 +208,7 @@ MemoryCache <- R6Class("MemoryCache",
}
private$log(paste0('get: key "', key, '" found'))
value <- private$cache[[key]]$value
value <- private$cache$get(key)$value
value
},
@@ -226,37 +226,36 @@ MemoryCache <- R6Class("MemoryCache",
size <- NULL
}
private$cache[[key]] <- list(
private$cache$set(key, list(
key = key,
value = value,
size = size,
mtime = time,
atime = time
)
))
self$prune()
invisible(self)
},
exists = function(key) {
validate_key(key)
# Faster than `exists(key, envir = private$cache, inherits = FALSE)
!is.null(private$cache[[key]])
private$cache$exists(key)
},
keys = function() {
ls(private$cache, sorted = FALSE) # Faster with sorted=FALSE
private$cache$keys()
},
remove = function(key) {
private$log(paste0('remove: key "', key, '"'))
validate_key(key)
rm(list = key, envir = private$cache)
private$cache$remove(key)
invisible(self)
},
reset = function() {
private$log(paste0('reset'))
rm(list = self$keys(), envir = private$cache)
private$cache$reset()
invisible(self)
},
@@ -271,7 +270,7 @@ MemoryCache <- R6Class("MemoryCache",
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)
private$cache$remove(info$key[rm_idx])
info <- info[!rm_idx, ]
}
}
@@ -298,7 +297,7 @@ MemoryCache <- R6Class("MemoryCache",
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)
private$cache$remove(info$key[rm_idx])
info <- info[!rm_idx, ]
}
@@ -308,7 +307,7 @@ MemoryCache <- R6Class("MemoryCache",
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)
private$cache$remove(info$key[rm_idx])
info <- info[!rm_idx, ]
}
@@ -335,23 +334,23 @@ MemoryCache <- R6Class("MemoryCache",
maybe_prune_single = function(key) {
if (!is.finite(private$max_age)) return()
obj <- private$cache[[key]]
obj <- private$cache$get(key)
if (is.null(obj)) return()
timediff <- as.numeric(Sys.time()) - obj$mtime
if (timediff > private$max_age) {
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
rm(list = key, envir = private$cache)
private$cache$remove(key)
}
},
object_info = function() {
keys <- ls(private$cache, sorted = FALSE)
keys <- private$cache$keys()
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),
size = vapply(keys, function(key) private$cache$get(key)$size, 0),
mtime = vapply(keys, function(key) private$cache$get(key)$mtime, 0),
atime = vapply(keys, function(key) private$cache$get(key)$atime, 0),
stringsAsFactors = FALSE
)
},

75
R/font-awesome.R Normal file
View File

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

View File

@@ -6,6 +6,12 @@
# package itself, making our PRNG completely deterministic. This line resets
# the private seed during load.
withPrivateSeed(set.seed(NULL))
appsByToken <<- Map$new()
appsNeedingFlush <<- Map$new()
timerCallbacks <<- TimerCallbacks$new()
initializeInputHandlers()
.globals$onStopCallbacks <<- Callbacks$new()
}
.onAttach <- function(libname, pkgname) {

588
R/graph.R
View File

@@ -1,13 +1,58 @@
writeReactLog <- function(file=stdout(), sessionToken = NULL) {
log <- .graphStack$as_list()
if (!is.null(sessionToken)) {
log <- Filter(function(x) {
is.null(x$session) || identical(x$session, sessionToken)
}, log)
}
cat(toJSON(log, pretty=TRUE), file=file)
is_installed <- function(package, version) {
installedVersion <- tryCatch(utils::packageVersion(package), error = function(e) NA)
!is.na(installedVersion) && installedVersion >= version
}
# Check that the version of an suggested package satisfies the requirements
#
# @param package The name of the suggested package
# @param version The version of the package
check_suggested <- function(package, version, location) {
if (is_installed(package, version)) {
return()
}
missing_location <- missing(location)
msg <- paste0(
sQuote(package),
if (is.na(version)) "" else paste0("(>= ", version, ")"),
" must be installed for this functionality.",
if (!missing_location)
paste0(
"\nPlease install the missing package: \n",
" source(\"https://install-github.me/", location, "\")"
)
)
if (interactive() && missing_location) {
message(msg, "\nWould you like to install it?")
if (utils::menu(c("Yes", "No")) == 1) {
return(utils::install.packages(package))
}
}
stop(msg, call. = FALSE)
}
# domain is like session
# used to help define truly global react id's.
# should work across session and in global namespace
.globals$reactIdCounter <- 0L
nextGlobalReactId <- function() {
.globals$reactIdCounter <- .globals$reactIdCounter + 1L
reactIdStr(.globals$reactIdCounter)
}
reactIdStr <- function(num) {
paste0("r", num)
}
#' Reactive Log Visualizer
#'
#' Provides an interactive browser-based tool for visualizing reactive
@@ -30,88 +75,499 @@ writeReactLog <- function(file=stdout(), sessionToken = NULL) {
#'
#' As an alternative to pressing Ctrl/Command+F3--for example, if you
#' are using reactives outside of the context of a Shiny
#' application--you can run the \code{showReactLog} function, which will
#' application--you can run the \code{reactlogShow} function, which will
#' generate the reactive log visualization as a static HTML file and
#' launch it in your default browser. In this case, refreshing your
#' browser will not load new activity into the report; you will need to
#' call \code{showReactLog()} explicitly.
#' call \code{reactlogShow()} explicitly.
#'
#' For security and performance reasons, do not enable
#' \code{shiny.reactlog} in production environments. When the option is
#' enabled, it's possible for any user of your app to see at least some
#' of the source code of your reactive expressions and observers.
#'
#' @param time A boolean that specifies whether or not to display the
#' time that each reactive.
#' @name reactlog
NULL
#' @describeIn reactlog Return a list of reactive information. Can be used in conjunction with
#' \code{reactlog::\link[reactlog]{reactlog_show}} to later display the reactlog graph.
#' @export
reactlog <- function() {
rLog$asList()
}
#' @describeIn reactlog Display a full reactlog graph for all sessions.
#' @inheritParams reactlog::reactlog_show
#' @export
reactlogShow <- function(time = TRUE) {
check_reactlog()
reactlog::reactlog_show(reactlog(), time = time)
}
#' @describeIn reactlog This function is deprecated. You should use \code{\link{reactlogShow}}
#' @export
# legacy purposes
showReactLog <- function(time = TRUE) {
utils::browseURL(renderReactLog(time = as.logical(time)))
shinyDeprecated(new = "`reactlogShow`", version = "1.2.0")
reactlogShow(time = time)
}
#' @describeIn reactlog Resets the entire reactlog stack. Useful for debugging and removing all prior reactive history.
#' @export
reactlogReset <- function() {
rLog$reset()
}
renderReactLog <- function(sessionToken = NULL, time = TRUE) {
templateFile <- system.file('www/reactive-graph.html', package='shiny')
html <- paste(readLines(templateFile, warn=FALSE), collapse='\r\n')
tc <- textConnection(NULL, 'w')
on.exit(close(tc))
writeReactLog(tc, sessionToken)
cat('\n', file=tc)
flush(tc)
html <- sub('__DATA__', paste(textConnectionValue(tc), collapse='\r\n'), html, fixed=TRUE)
html <- sub('__TIME__', paste0('"', time, '"'), html, fixed=TRUE)
file <- tempfile(fileext = '.html')
writeLines(html, file)
return(file)
# called in "/reactlog" middleware
renderReactlog <- function(sessionToken = NULL, time = TRUE) {
check_reactlog()
reactlog::reactlog_render(
reactlog(),
session_token = sessionToken,
time = time
)
}
check_reactlog <- function() {
check_suggested("reactlog", reactlog_version())
}
# read reactlog version from description file
# prevents version mismatch in code and description file
reactlog_version <- function() {
desc <- read.dcf(system.file("DESCRIPTION", package = "shiny", mustWork = TRUE))
suggests <- desc[1,"Suggests"][[1]]
suggests_pkgs <- strsplit(suggests, "\n")[[1]]
.graphAppend <- function(logEntry, domain = getDefaultReactiveDomain()) {
if (isTRUE(getOption('shiny.reactlog'))) {
sessionToken <- if (is.null(domain)) NULL else domain$token
.graphStack$push(c(logEntry, list(
session = sessionToken,
time = as.numeric(Sys.time())
)))
reactlog_info <- suggests_pkgs[grepl("reactlog", suggests_pkgs)]
if (length(reactlog_info) == 0) {
stop("reactlog can not be found in shiny DESCRIPTION file")
}
if (!is.null(domain)) {
domain$reactlog(logEntry)
}
reactlog_info <- sub("^[^\\(]*\\(", "", reactlog_info)
reactlog_info <- sub("\\)[^\\)]*$", "", reactlog_info)
reactlog_info <- sub("^[>= ]*", "", reactlog_info)
package_version(reactlog_info)
}
.graphDependsOn <- function(id, label) {
.graphAppend(list(action='dep', id=id, dependsOn=label))
}
.graphDependsOnId <- function(id, dependee) {
.graphAppend(list(action='depId', id=id, dependsOn=dependee))
}
RLog <- R6Class(
"RLog",
portable = FALSE,
private = list(
option = "shiny.reactlog",
msgOption = "shiny.reactlog.console",
.graphCreateContext <- function(id, label, type, prevId, domain) {
.graphAppend(list(
action='ctx', id=id, label=paste(label, collapse='\n'),
srcref=as.vector(attr(label, "srcref")), srcfile=attr(label, "srcfile"),
type=type, prevId=prevId
), domain = domain)
}
appendEntry = function(domain, logEntry) {
if (self$isLogging()) {
sessionToken <- if (is.null(domain)) NULL else domain$token
logStack$push(c(logEntry, list(
session = sessionToken,
time = as.numeric(Sys.time())
)))
}
if (!is.null(domain)) domain$reactlog(logEntry)
}
),
public = list(
msg = "<MessageLogger>",
logStack = "<Stack>",
.graphEnterContext <- function(id) {
.graphAppend(list(action='enter', id=id))
}
noReactIdLabel = "NoCtxReactId",
noReactId = reactIdStr("NoCtxReactId"),
dummyReactIdLabel = "DummyReactId",
dummyReactId = reactIdStr("DummyReactId"),
.graphExitContext <- function(id, domain) {
.graphAppend(list(action='exit', id=id), domain = domain)
}
asList = function() {
ret <- self$logStack$as_list()
attr(ret, "version") <- "1"
ret
},
.graphValueChange <- function(label, value) {
.graphAppend(list(
action = 'valueChange',
id = label,
value = paste(utils::capture.output(utils::str(value)), collapse='\n')
))
}
ctxIdStr = function(ctxId) {
if (is.null(ctxId) || identical(ctxId, "")) return(NULL)
paste0("ctx", ctxId)
},
namesIdStr = function(reactId) {
paste0("names(", reactId, ")")
},
asListIdStr = function(reactId) {
paste0("as.list(", reactId, ")")
},
asListAllIdStr = function(reactId) {
paste0("as.list(", reactId, ", all.names = TRUE)")
},
keyIdStr = function(reactId, key) {
paste0(reactId, "$", key)
},
.graphInvalidate <- function(id, domain) {
.graphAppend(list(action='invalidate', id=id), domain)
}
valueStr = function(value, n = 200) {
if (!self$isLogging()) {
# return a placeholder string to avoid calling str
return("<reactlog is turned off>")
}
output <- try(silent = TRUE, {
# only capture the first level of the object
utils::capture.output(utils::str(value, max.level = 1))
})
outputTxt <- paste0(output, collapse="\n")
msg$shortenString(outputTxt, n = n)
},
initialize = function(rlogOption = "shiny.reactlog", msgOption = "shiny.reactlog.console") {
private$option <- rlogOption
private$msgOption <- msgOption
self$reset()
},
reset = function() {
.globals$reactIdCounter <- 0L
self$logStack <- Stack$new()
self$msg <- MessageLogger$new(option = private$msgOption)
# setup dummy and missing react information
self$msg$setReact(force = TRUE, list(reactId = self$noReactId, label = self$noReactIdLabel))
self$msg$setReact(force = TRUE, list(reactId = self$dummyReactId, label = self$dummyReactIdLabel))
},
isLogging = function() {
isTRUE(getOption(private$option, FALSE))
},
define = function(reactId, value, label, type, domain) {
valueStr <- self$valueStr(value)
if (msg$hasReact(reactId)) {
stop("react definition for id: ", reactId, " already found!!", "Label: ", label, "Type: ", type)
}
msg$setReact(list(reactId = reactId, label = label))
msg$log("define:", msg$reactStr(reactId), msg$typeStr(type = type), msg$valueStr(valueStr))
private$appendEntry(domain, list(
action = "define",
reactId = reactId,
label = msg$shortenString(label),
type = type,
value = valueStr
))
},
defineNames = function(reactId, value, label, domain) {
self$define(self$namesIdStr(reactId), value, self$namesIdStr(label), "reactiveValuesNames", domain)
},
defineAsList = function(reactId, value, label, domain) {
self$define(self$asListIdStr(reactId), value, self$asListIdStr(label), "reactiveValuesAsList", domain)
},
defineAsListAll = function(reactId, value, label, domain) {
self$define(self$asListAllIdStr(reactId), value, self$asListAllIdStr(label), "reactiveValuesAsListAll", domain)
},
defineKey = function(reactId, value, key, label, domain) {
self$define(self$keyIdStr(reactId, key), value, self$keyIdStr(label, key), "reactiveValuesKey", domain)
},
defineObserver = function(reactId, label, domain) {
self$define(reactId, value = NULL, label, "observer", domain)
},
dependsOn = function(reactId, depOnReactId, ctxId, domain) {
if (is.null(reactId)) return()
ctxId <- ctxIdStr(ctxId)
msg$log("dependsOn:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId))
private$appendEntry(domain, list(
action = "dependsOn",
reactId = reactId,
depOnReactId = depOnReactId,
ctxId = ctxId
))
},
dependsOnKey = function(reactId, depOnReactId, key, ctxId, domain) {
self$dependsOn(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain)
},
dependsOnRemove = function(reactId, depOnReactId, ctxId, domain) {
ctxId <- self$ctxIdStr(ctxId)
msg$log("dependsOnRemove:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId))
private$appendEntry(domain, list(
action = "dependsOnRemove",
reactId = reactId,
depOnReactId = depOnReactId,
ctxId = ctxId
))
},
dependsOnKeyRemove = function(reactId, depOnReactId, key, ctxId, domain) {
self$dependsOnRemove(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain)
},
createContext = function(ctxId, label, type, prevCtxId, domain) {
ctxId <- self$ctxIdStr(ctxId)
prevCtxId <- self$ctxIdStr(prevCtxId)
msg$log("createContext:", msg$ctxPrevCtxStr(preCtxIdTxt = " ", ctxId, prevCtxId, type))
private$appendEntry(domain, list(
action = "createContext",
ctxId = ctxId,
label = msg$shortenString(label),
type = type,
prevCtxId = prevCtxId,
srcref = as.vector(attr(label, "srcref")), srcfile=attr(label, "srcfile")
))
},
enter = function(reactId, ctxId, type, domain) {
ctxId <- self$ctxIdStr(ctxId)
if (identical(type, "isolate")) {
msg$log("isolateEnter:", msg$reactStr(reactId), msg$ctxStr(ctxId))
msg$depthIncrement()
private$appendEntry(domain, list(
action = "isolateEnter",
reactId = reactId,
ctxId = ctxId
))
} else {
msg$log("enter:", msg$reactStr(reactId), msg$ctxStr(ctxId, type))
msg$depthIncrement()
private$appendEntry(domain, list(
action = "enter",
reactId = reactId,
ctxId = ctxId,
type = type
))
}
},
exit = function(reactId, ctxId, type, domain) {
ctxId <- self$ctxIdStr(ctxId)
if (identical(type, "isolate")) {
msg$depthDecrement()
msg$log("isolateExit:", msg$reactStr(reactId), msg$ctxStr(ctxId))
private$appendEntry(domain, list(
action = "isolateExit",
reactId = reactId,
ctxId = ctxId
))
} else {
msg$depthDecrement()
msg$log("exit:", msg$reactStr(reactId), msg$ctxStr(ctxId, type))
private$appendEntry(domain, list(
action = "exit",
reactId = reactId,
ctxId = ctxId,
type = type
))
}
},
valueChange = function(reactId, value, domain) {
valueStr <- self$valueStr(value)
msg$log("valueChange:", msg$reactStr(reactId), msg$valueStr(valueStr))
private$appendEntry(domain, list(
action = "valueChange",
reactId = reactId,
value = valueStr
))
},
valueChangeNames = function(reactId, nameValues, domain) {
self$valueChange(self$namesIdStr(reactId), nameValues, domain)
},
valueChangeAsList = function(reactId, listValue, domain) {
self$valueChange(self$asListIdStr(reactId), listValue, domain)
},
valueChangeAsListAll = function(reactId, listValue, domain) {
self$valueChange(self$asListAllIdStr(reactId), listValue, domain)
},
valueChangeKey = function(reactId, key, value, domain) {
self$valueChange(self$keyIdStr(reactId, key), value, domain)
},
invalidateStart = function(reactId, ctxId, type, domain) {
ctxId <- self$ctxIdStr(ctxId)
if (identical(type, "isolate")) {
msg$log("isolateInvalidateStart:", msg$reactStr(reactId), msg$ctxStr(ctxId))
msg$depthIncrement()
private$appendEntry(domain, list(
action = "isolateInvalidateStart",
reactId = reactId,
ctxId = ctxId
))
} else {
msg$log("invalidateStart:", msg$reactStr(reactId), msg$ctxStr(ctxId, type))
msg$depthIncrement()
private$appendEntry(domain, list(
action = "invalidateStart",
reactId = reactId,
ctxId = ctxId,
type = type
))
}
},
invalidateEnd = function(reactId, ctxId, type, domain) {
ctxId <- self$ctxIdStr(ctxId)
if (identical(type, "isolate")) {
msg$depthDecrement()
msg$log("isolateInvalidateEnd:", msg$reactStr(reactId), msg$ctxStr(ctxId))
private$appendEntry(domain, list(
action = "isolateInvalidateEnd",
reactId = reactId,
ctxId = ctxId
))
} else {
msg$depthDecrement()
msg$log("invalidateEnd:", msg$reactStr(reactId), msg$ctxStr(ctxId, type))
private$appendEntry(domain, list(
action = "invalidateEnd",
reactId = reactId,
ctxId = ctxId,
type = type
))
}
},
invalidateLater = function(reactId, runningCtx, millis, domain) {
msg$log("invalidateLater: ", millis, "ms", msg$reactStr(reactId), msg$ctxStr(runningCtx))
private$appendEntry(domain, list(
action = "invalidateLater",
reactId = reactId,
ctxId = runningCtx,
millis = millis
))
},
idle = function(domain = NULL) {
msg$log("idle")
private$appendEntry(domain, list(
action = "idle"
))
},
asyncStart = function(domain = NULL) {
msg$log("asyncStart")
private$appendEntry(domain, list(
action = "asyncStart"
))
},
asyncStop = function(domain = NULL) {
msg$log("asyncStop")
private$appendEntry(domain, list(
action = "asyncStop"
))
},
freezeReactiveVal = function(reactId, domain) {
msg$log("freeze:", msg$reactStr(reactId))
private$appendEntry(domain, list(
action = "freeze",
reactId = reactId
))
},
freezeReactiveKey = function(reactId, key, domain) {
self$freezeReactiveVal(self$keyIdStr(reactId, key), domain)
},
thawReactiveVal = function(reactId, domain) {
msg$log("thaw:", msg$reactStr(reactId))
private$appendEntry(domain, list(
action = "thaw",
reactId = reactId
))
},
thawReactiveKey = function(reactId, key, domain) {
self$thawReactiveVal(self$keyIdStr(reactId, key), domain)
},
userMark = function(domain = NULL) {
msg$log("userMark")
private$appendEntry(domain, list(
action = "userMark"
))
}
)
)
MessageLogger = R6Class(
"MessageLogger",
portable = FALSE,
public = list(
depth = 0L,
reactCache = list(),
option = "shiny.reactlog.console",
initialize = function(option = "shiny.reactlog.console", depth = 0L) {
if (!missing(depth)) self$depth <- depth
if (!missing(option)) self$option <- option
},
isLogging = function() {
isTRUE(getOption(self$option))
},
isNotLogging = function() {
! isTRUE(getOption(self$option))
},
depthIncrement = function() {
if (self$isNotLogging()) return(NULL)
self$depth <- self$depth + 1L
},
depthDecrement = function() {
if (self$isNotLogging()) return(NULL)
self$depth <- self$depth - 1L
},
hasReact = function(reactId) {
if (self$isNotLogging()) return(FALSE)
!is.null(self$getReact(reactId))
},
getReact = function(reactId, force = FALSE) {
if (identical(force, FALSE) && self$isNotLogging()) return(NULL)
self$reactCache[[reactId]]
},
setReact = function(reactObj, force = FALSE) {
if (identical(force, FALSE) && self$isNotLogging()) return(NULL)
self$reactCache[[reactObj$reactId]] <- reactObj
},
shortenString = function(txt, n = 250) {
if (is.null(txt) || isTRUE(is.na(txt))) {
return("")
}
if (nchar(txt) > n) {
return(
paste0(substr(txt, 1, n - 3), "...")
)
}
return(txt)
},
singleLine = function(txt) {
gsub("[^\\]\\n", "\\\\n", txt)
},
valueStr = function(valueStr) {
paste0(
" '", self$shortenString(self$singleLine(valueStr)), "'"
)
},
reactStr = function(reactId) {
if (self$isNotLogging()) return(NULL)
reactInfo <- self$getReact(reactId)
if (is.null(reactInfo)) return(" <UNKNOWN_REACTID>")
paste0(
" ", reactInfo$reactId, ":'", self$shortenString(self$singleLine(reactInfo$label)), "'"
)
},
typeStr = function(type = NULL) {
self$ctxStr(ctxId = NULL, type = type)
},
ctxStr = function(ctxId = NULL, type = NULL) {
if (self$isNotLogging()) return(NULL)
self$ctxPrevCtxStr(ctxId = ctxId, prevCtxId = NULL, type = type)
},
ctxPrevCtxStr = function(ctxId = NULL, prevCtxId = NULL, type = NULL, preCtxIdTxt = " in ") {
if (self$isNotLogging()) return(NULL)
paste0(
if (!is.null(ctxId)) paste0(preCtxIdTxt, ctxId),
if (!is.null(prevCtxId)) paste0(" from ", prevCtxId),
if (!is.null(type) && !identical(type, "other")) paste0(" - ", type)
)
},
log = function(...) {
if (self$isNotLogging()) return(NULL)
msg <- paste0(
paste0(rep("= ", depth), collapse = ""), "- ", paste0(..., collapse = ""),
collapse = ""
)
message(msg)
}
)
)
#' @include stack.R
.graphStack <- Stack$new()
rLog <- RLog$new("shiny.reactlog", "shiny.reactlog.console")

View File

@@ -94,7 +94,7 @@ checkboxGroupInput <- function(inputId, label, choices = NULL, selected = NULL,
tags$div(id = inputId,
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
class = divClass,
controlLabel(inputId, label),
shinyInputLabel(inputId, label),
options
)
}

View File

@@ -78,7 +78,7 @@
#'
#' # 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"))
@@ -92,14 +92,10 @@ dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
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 <- dateYMD(value, "value")
min <- dateYMD(min, "min")
max <- dateYMD(max, "max")
datesdisabled <- dateYMD(datesdisabled, "datesdisabled")
value <- restoreInput(id = inputId, default = value)
@@ -107,7 +103,7 @@ dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
class = "shiny-date-input form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
controlLabel(inputId, label),
shinyInputLabel(inputId, label),
tags$input(type = "text",
class = "form-control",
`data-date-language` = language,

View File

@@ -76,12 +76,10 @@ dateRangeInput <- function(inputId, label, start = NULL, end = 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
if (inherits(start, "Date")) start <- format(start, "%Y-%m-%d")
if (inherits(end, "Date")) end <- format(end, "%Y-%m-%d")
if (inherits(min, "Date")) min <- format(min, "%Y-%m-%d")
if (inherits(max, "Date")) max <- format(max, "%Y-%m-%d")
start <- dateYMD(start, "start")
end <- dateYMD(end, "end")
min <- dateYMD(min, "min")
max <- dateYMD(max, "max")
restored <- restoreInput(id = inputId, default = list(start, end))
start <- restored[[1]]
@@ -92,7 +90,7 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
class = "shiny-date-range-input form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
controlLabel(inputId, label),
shinyInputLabel(inputId, label),
# input-daterange class is needed for dropdown behavior
div(class = "input-daterange input-group",
tags$input(

View File

@@ -103,7 +103,7 @@ fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
div(class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
label %AND% tags$label(label),
shinyInputLabel(inputId, label),
div(class = "input-group",
tags$label(class = "input-group-btn",

View File

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

View File

@@ -30,7 +30,7 @@ passwordInput <- function(inputId, label, value = "", width = NULL,
placeholder = NULL) {
div(class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
label %AND% tags$label(label, `for` = inputId),
shinyInputLabel(inputId, label),
tags$input(id = inputId, type="password", class="form-control", value=value,
placeholder = placeholder)
)

View File

@@ -102,7 +102,7 @@ radioButtons <- function(inputId, label, choices = NULL, selected = NULL,
tags$div(id = inputId,
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
class = divClass,
controlLabel(inputId, label),
shinyInputLabel(inputId, label),
options
)
}

View File

@@ -5,7 +5,7 @@
#'
#' By default, \code{selectInput()} and \code{selectizeInput()} use the
#' JavaScript library \pkg{selectize.js}
#' (\url{https://github.com/selectize/selectize.js}) to instead of the basic
#' (\url{https://github.com/selectize/selectize.js}) instead of the basic
#' select input element. To use the standard HTML select input element, use
#' \code{selectInput()} with \code{selectize=FALSE}.
#'
@@ -105,7 +105,7 @@ selectInput <- function(inputId, label, choices, selected = NULL,
res <- div(
class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
controlLabel(inputId, label),
shinyInputLabel(inputId, label),
div(selectTag)
)

View File

@@ -172,7 +172,7 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
sliderTag <- div(class = "form-group shiny-input-container",
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
if (!is.null(label)) controlLabel(inputId, label),
shinyInputLabel(inputId, label),
do.call(tags$input, sliderProps)
)

View File

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

View File

@@ -55,7 +55,7 @@ textAreaInput <- function(inputId, label, value = "", width = NULL, height = NUL
if (length(style) == 0) style <- NULL
div(class = "form-group shiny-input-container",
label %AND% tags$label(label, `for` = inputId),
shinyInputLabel(inputId, label),
tags$textarea(
id = inputId,
class = "form-control",

View File

@@ -1,5 +1,10 @@
controlLabel <- function(controlName, label) {
label %AND% tags$label(class = "control-label", `for` = controlName, label)
shinyInputLabel <- function(inputId, label = NULL) {
tags$label(
label,
class = "control-label",
class = if (is.null(label)) "shiny-label-null",
`for` = inputId
)
}
# This function takes in either a list or vector for `choices` (and

43
R/map.R
View File

@@ -9,63 +9,58 @@
# Remove of unknown key does nothing
# Setting a key twice always results in last-one-wins
# /TESTS
# Note that Map objects can't be saved in one R session and restored in
# another, because they are based on fastmap, which uses an external pointer,
# and external pointers can't be saved and restored in another session.
#' @importFrom fastmap fastmap
Map <- R6Class(
'Map',
portable = FALSE,
public = list(
initialize = function() {
private$env <- new.env(parent=emptyenv())
private$map <<- fastmap()
},
get = function(key) {
env[[key]]
map$get(key)
},
set = function(key, value) {
env[[key]] <- value
map$set(key, value)
value
},
mget = function(keys) {
base::mget(keys, env)
map$mget(keys)
},
mset = function(...) {
args <- list(...)
if (length(args) == 0)
return()
arg_names <- names(args)
if (is.null(arg_names) || any(!nzchar(arg_names)))
stop("All elements must be named")
list2env(args, envir = env)
map$mset(...)
},
remove = function(key) {
if (!self$containsKey(key))
if (!map$exists(key))
return(NULL)
result <- env[[key]]
rm(list=key, envir=env, inherits=FALSE)
result <- map$get(key)
map$remove(key)
result
},
containsKey = function(key) {
exists(key, envir=env, inherits=FALSE)
map$exists(key)
},
keys = function() {
# Sadly, this is much faster than ls(), because it doesn't sort the keys.
names(as.list(env, all.names=TRUE))
map$keys()
},
values = function() {
as.list(env, all.names=TRUE)
map$as_list()
},
clear = function() {
private$env <- new.env(parent=emptyenv())
invisible(NULL)
map$reset()
},
size = function() {
length(env)
map$size()
}
),
private = list(
env = 'environment'
map = NULL
)
)

View File

@@ -2,19 +2,43 @@
NULL
reactLogHandler <- function(req) {
if (!identical(req$PATH_INFO, '/reactlog'))
return(NULL)
if (!isTRUE(getOption('shiny.reactlog'))) {
if (! rLog$isLogging()) {
return(NULL)
}
sessionToken <- parseQueryString(req$QUERY_STRING)$s
if (identical(req$PATH_INFO, "/reactlog/mark")) {
sessionToken <- parseQueryString(req$QUERY_STRING)$s
shinysession <- appsByToken$get(sessionToken)
return(httpResponse(
status=200,
content=list(file=renderReactLog(sessionToken), owned=TRUE)
))
# log time
withReactiveDomain(shinysession, {
rLog$userMark(getDefaultReactiveDomain())
})
return(httpResponse(
status = 200,
content = "marked",
content_type = "text/plain"
))
} else if (identical(req$PATH_INFO, "/reactlog")){
sessionToken <- parseQueryString(req$QUERY_STRING)$s
# `renderReactLog` will check/throw if reactlog doesn't exist
reactlogFile <- renderReactlog(sessionToken)
return(httpResponse(
status = 200,
content = list(
file = reactlogFile,
owned = TRUE
)
))
} else {
return(NULL)
}
}
sessionHandler <- function(req) {

View File

@@ -321,21 +321,20 @@ HandlerManager <- R6Class("HandlerManager",
}
)
},
getOption('shiny.sharedSecret')
loadSharedSecret()
),
onWSOpen = function(ws) {
return(wsHandlers$invoke(ws))
}
)
},
.httpServer = function(handler, sharedSecret) {
.httpServer = function(handler, checkSharedSecret) {
filter <- getOption('shiny.http.response.filter')
if (is.null(filter))
filter <- function(req, response) response
function(req) {
if (!is.null(sharedSecret)
&& !identical(sharedSecret, req$HTTP_SHINY_SHARED_SECRET)) {
if (!checkSharedSecret(req$HTTP_SHINY_SHARED_SECRET)) {
return(list(status=403,
body='<h1>403 Forbidden</h1><p>Shared secret mismatch</p>',
headers=list('Content-Type' = 'text/html')))

View File

@@ -16,12 +16,15 @@ processId <- local({
}
})
#' @include graph.R
Context <- R6Class(
'Context',
portable = FALSE,
class = FALSE,
public = list(
id = character(0),
.reactId = character(0),
.reactType = "other",
.label = character(0), # For debug purposes
.invalidated = FALSE,
.invalidateCallbacks = list(),
@@ -29,12 +32,18 @@ Context <- R6Class(
.domain = NULL,
.pid = NULL,
initialize = function(domain, label='', type='other', prevId='') {
id <<- .getReactiveEnvironment()$nextId()
initialize = function(
domain, label='', type='other', prevId='',
reactId = rLog$noReactId,
id = .getReactiveEnvironment()$nextId() # For dummy context
) {
id <<- id
.label <<- label
.domain <<- domain
.pid <<- processId()
.graphCreateContext(id, label, type, prevId, domain)
.reactId <<- reactId
.reactType <<- type
rLog$createContext(id, label, type, prevId, domain)
},
run = function(func) {
"Run the provided function under this context."
@@ -42,10 +51,8 @@ Context <- R6Class(
promises::with_promise_domain(reactivePromiseDomain(), {
withReactiveDomain(.domain, {
env <- .getReactiveEnvironment()
.graphEnterContext(id)
on.exit({
.graphExitContext(id, domain = .domain)
}, add = TRUE)
rLog$enter(.reactId, id, .reactType, .domain)
on.exit(rLog$exit(.reactId, id, .reactType, .domain), add = TRUE)
env$runWith(self, func)
})
})
@@ -62,7 +69,9 @@ Context <- R6Class(
return()
.invalidated <<- TRUE
.graphInvalidate(id, .domain)
rLog$invalidateStart(.reactId, id, .reactType, .domain)
on.exit(rLog$invalidateEnd(.reactId, id, .reactType, .domain), add = TRUE)
lapply(.invalidateCallbacks, function(func) {
func()
})
@@ -151,7 +160,10 @@ ReactiveEnvironment <- R6Class(
# If already in a flush, don't start another one
if (.inFlush) return(invisible(FALSE))
.inFlush <<- TRUE
on.exit(.inFlush <<- FALSE)
on.exit({
.inFlush <<- FALSE
rLog$idle(domain = NULL)
})
while (hasPendingFlush()) {
ctx <- .pendingFlush$dequeue()
@@ -183,18 +195,16 @@ flushReact <- function() {
getCurrentContext <- function() {
.getReactiveEnvironment()$currentContext()
}
hasCurrentContext <- function() {
!is.null(.getReactiveEnvironment()$.currentContext)
}
getDummyContext <- function() {}
local({
dummyContext <- NULL
getDummyContext <<- function() {
if (is.null(dummyContext)) {
dummyContext <<- Context$new(getDefaultReactiveDomain(), '[none]',
type='isolate')
}
return(dummyContext)
}
})
getDummyContext <- function() {
Context$new(
getDefaultReactiveDomain(), '[none]', type = 'isolate',
id = "Dummy", reactId = rLog$dummyReactId
)
}
wrapForContext <- function(func, ctx) {
force(func)

View File

@@ -6,26 +6,43 @@ Dependents <- R6Class(
portable = FALSE,
class = FALSE,
public = list(
.reactId = character(0),
.dependents = 'Map',
initialize = function() {
initialize = function(reactId = NULL) {
.reactId <<- reactId
.dependents <<- Map$new()
},
register = function(depId=NULL, depLabel=NULL) {
ctx <- .getReactiveEnvironment()$currentContext()
# ... ignored, use to be depLabel and depId, not used anymore
register = function(...) {
ctx <- getCurrentContext()
if (!.dependents$containsKey(ctx$id)) {
# must wrap in if statement as ctx react id could be NULL
# if options(shiny.suppressMissingContextError = TRUE)
if (is.character(.reactId) && is.character(ctx$.reactId)) {
# rLog$dependsOn(ctx$.reactId, .reactId, ctx$id, ctx$.domain)
}
.dependents$set(ctx$id, ctx)
ctx$onInvalidate(function() {
# rLog$dependsOnRemove(ctx$.reactId, .reactId, ctx$id, ctx$.domain)
.dependents$remove(ctx$id)
})
if (!is.null(depId) && nchar(depId) > 0)
.graphDependsOnId(ctx$id, depId)
if (!is.null(depLabel))
.graphDependsOn(ctx$id, depLabel)
}
},
invalidate = function() {
# at times, the context is run in a ctx$onInvalidate(...) which has no runtime context
invalidate = function(log = TRUE) {
# if (isTRUE(log)) {
# domain <- getDefaultReactiveDomain()
# rLog$invalidateStart(.reactId, NULL, "other", domain)
# on.exit(
# rLog$invalidateEnd(.reactId, NULL, "other", domain),
# add = TRUE
# )
# }
lapply(
.dependents$values(),
function(ctx) {
@@ -44,6 +61,7 @@ ReactiveVal <- R6Class(
'ReactiveVal',
portable = FALSE,
private = list(
reactId = character(0),
value = NULL,
label = NULL,
frozen = FALSE,
@@ -51,13 +69,15 @@ ReactiveVal <- R6Class(
),
public = list(
initialize = function(value, label = NULL) {
reactId <- nextGlobalReactId()
private$reactId <- reactId
private$value <- value
private$label <- label
private$dependents <- Dependents$new()
.graphValueChange(private$label, value)
private$dependents <- Dependents$new(reactId = private$reactId)
# rLog$define(private$reactId, value, private$label, type = "reactiveVal", getDefaultReactiveDomain())
},
get = function() {
private$dependents$register(depLabel = private$label)
private$dependents$register()
if (private$frozen)
reactiveStop()
@@ -68,8 +88,8 @@ ReactiveVal <- R6Class(
if (identical(private$value, value)) {
return(invisible(FALSE))
}
# rLog$valueChange(private$reactId, value, getDefaultReactiveDomain())
private$value <- value
.graphValueChange(private$label, value)
private$dependents$invalidate()
invisible(TRUE)
},
@@ -77,12 +97,14 @@ ReactiveVal <- R6Class(
if (is.null(session)) {
stop("Can't freeze a reactiveVal without a reactive domain")
}
# rLog$freezeReactiveVal(private$reactId, session)
session$onFlushed(function() {
self$thaw()
self$thaw(session)
})
private$frozen <- TRUE
},
thaw = function() {
thaw = function(session = getDefaultReactiveDomain()) {
# rLog$thawReactiveVal(private$reactId, session)
private$frozen <- FALSE
},
isFrozen = function() {
@@ -118,7 +140,7 @@ ReactiveVal <- R6Class(
#'
#' @param value An optional initial value.
#' @param label An optional label, for debugging purposes (see
#' \code{\link{showReactLog}}). If missing, a label will be automatically
#' \code{\link{reactlog}}). If missing, a label will be automatically
#' created.
#'
#' @return A function. Call the function with no arguments to (reactively) read
@@ -268,10 +290,11 @@ ReactiveValues <- R6Class(
portable = FALSE,
public = list(
# For debug purposes
.reactId = character(0),
.label = character(0),
.values = 'environment',
.metadata = 'environment',
.dependents = 'environment',
.values = 'Map',
.metadata = 'Map',
.dependents = 'Map',
# Dependents for the list of all names, including hidden
.namesDeps = 'Dependents',
# Dependents for all values, including hidden
@@ -279,72 +302,132 @@ ReactiveValues <- R6Class(
# Dependents for all values
.valuesDeps = 'Dependents',
.dedupe = logical(0),
# Key, asList(), or names() have been retrieved
.hasRetrieved = list(),
initialize = function(dedupe = TRUE) {
.label <<- paste('reactiveValues',
p_randomInt(1000, 10000),
sep="")
.values <<- new.env(parent=emptyenv())
.metadata <<- new.env(parent=emptyenv())
.dependents <<- new.env(parent=emptyenv())
.namesDeps <<- Dependents$new()
.allValuesDeps <<- Dependents$new()
.valuesDeps <<- Dependents$new()
initialize = function(
dedupe = TRUE,
label = paste0('reactiveValues', p_randomInt(1000, 10000))
) {
.reactId <<- nextGlobalReactId()
.label <<- label
.values <<- Map$new()
.metadata <<- Map$new()
.dependents <<- Map$new()
.hasRetrieved <<- list(names = FALSE, asListAll = FALSE, asList = FALSE, keys = list())
.namesDeps <<- Dependents$new(reactId = rLog$namesIdStr(.reactId))
.allValuesDeps <<- Dependents$new(reactId = rLog$asListAllIdStr(.reactId))
.valuesDeps <<- Dependents$new(reactId = rLog$asListIdStr(.reactId))
.dedupe <<- dedupe
},
get = function(key) {
# get value right away to use for logging
keyValue <- .values$get(key)
# Register the "downstream" reactive which is accessing this value, so
# that we know to invalidate them when this value changes.
ctx <- .getReactiveEnvironment()$currentContext()
ctx <- getCurrentContext()
dep.key <- paste(key, ':', ctx$id, sep='')
if (!exists(dep.key, envir=.dependents, inherits=FALSE)) {
.graphDependsOn(ctx$id, sprintf('%s$%s', .label, key))
.dependents[[dep.key]] <- ctx
if (!.dependents$containsKey(dep.key)) {
# reactKeyId <- rLog$keyIdStr(.reactId, key)
if (!isTRUE(.hasRetrieved$keys[[key]])) {
# rLog$defineKey(.reactId, keyValue, key, .label, ctx$.domain)
.hasRetrieved$keys[[key]] <<- TRUE
}
# rLog$dependsOnKey(ctx$.reactId, .reactId, key, ctx$id, ctx$.domain)
.dependents$set(dep.key, ctx)
ctx$onInvalidate(function() {
rm(list=dep.key, envir=.dependents, inherits=FALSE)
# rLog$dependsOnKeyRemove(ctx$.reactId, .reactId, key, ctx$id, ctx$.domain)
.dependents$remove(dep.key)
})
}
if (isFrozen(key))
reactiveStop()
if (!exists(key, envir=.values, inherits=FALSE))
NULL
else
.values[[key]]
keyValue
},
set = function(key, value) {
# if key exists
# if it is the same value, return
#
# update value of `key`
#
# if key exists
# if `key` has been read,
# log `update key`
# ## (invalidate key later in code)
# else # if new key
# if `names()` have been read,
# log `update names()`
# invalidate `names()`
#
# if hidden
# if asListAll has been read,
# log `update asList(all.names = TRUE)`
# invalidate `asListAll`
# else # not hidden
# if asList has been read,
# log `update asList()`
# invalidate `asList`
#
# update value of `key`
# invalidate all deps of `key`
domain <- getDefaultReactiveDomain()
hidden <- substr(key, 1, 1) == "."
if (exists(key, envir=.values, inherits=FALSE)) {
if (.dedupe && identical(.values[[key]], value)) {
key_exists <- .values$containsKey(key)
if (key_exists) {
if (.dedupe && identical(.values$get(key), value)) {
return(invisible())
}
}
else {
# set the value for better logging
.values$set(key, value)
# key has been depended upon
# if (isTRUE(.hasRetrieved$keys[[key]])) {
# rLog$valueChangeKey(.reactId, key, value, domain)
# keyReactId <- rLog$keyIdStr(.reactId, key)
# rLog$invalidateStart(keyReactId, NULL, "other", domain)
# on.exit(
# rLog$invalidateEnd(keyReactId, NULL, "other", domain),
# add = TRUE
# )
# }
# only invalidate if there are deps
if (!key_exists && isTRUE(.hasRetrieved$names)) {
# rLog$valueChangeNames(.reactId, .values$keys(), domain)
.namesDeps$invalidate()
}
if (hidden)
.allValuesDeps$invalidate()
else
.valuesDeps$invalidate()
if (hidden) {
if (isTRUE(.hasRetrieved$asListAll)) {
# rLog$valueChangeAsListAll(.reactId, .values$values(), domain)
.allValuesDeps$invalidate()
}
} else {
if (isTRUE(.hasRetrieved$asList)) {
react_vals <- .values$values()
react_vals <- react_vals[!grepl("^\\.", base::names(react_vals))]
# leave as is. both object would be registered to the listening object
# rLog$valueChangeAsList(.reactId, react_vals, domain)
.valuesDeps$invalidate()
}
}
.values[[key]] <- value
.graphValueChange(sprintf('names(%s)', .label), ls(.values, all.names=TRUE))
.graphValueChange(sprintf('%s (all)', .label), as.list(.values))
.graphValueChange(sprintf('%s$%s', .label, key), value)
dep.keys <- objects(
envir=.dependents,
pattern=paste('^\\Q', key, ':', '\\E', '\\d+$', sep=''),
all.names=TRUE
)
dep.keys <- .dependents$keys()
dep.keys <- dep.keys[grepl(paste('^\\Q', key, ':', '\\E', '\\d+$', sep=''), dep.keys)]
lapply(
mget(dep.keys, envir=.dependents),
.dependents$mget(dep.keys),
function(ctx) {
ctx$invalidate()
NULL
@@ -361,17 +444,21 @@ ReactiveValues <- R6Class(
},
names = function() {
.graphDependsOn(.getReactiveEnvironment()$currentContext()$id,
sprintf('names(%s)', .label))
nameValues <- .values$keys()
if (!isTRUE(.hasRetrieved$names)) {
domain <- getDefaultReactiveDomain()
# rLog$defineNames(.reactId, nameValues, .label, domain)
.hasRetrieved$names <<- TRUE
}
.namesDeps$register()
return(ls(.values, all.names=TRUE))
return(nameValues)
},
# Get a metadata value. Does not trigger reactivity.
getMeta = function(key, metaKey) {
# Make sure to use named (not numeric) indexing into list.
metaKey <- as.character(metaKey)
.metadata[[key]][[metaKey]]
.metadata$get("key")[[metaKey]]
},
# Set a metadata value. Does not trigger reactivity.
@@ -379,20 +466,26 @@ ReactiveValues <- R6Class(
# Make sure to use named (not numeric) indexing into list.
metaKey <- as.character(metaKey)
if (!exists(key, envir = .metadata, inherits = FALSE)) {
.metadata[[key]] <<- list()
if (!.metadata$containsKey(key)) {
.metadata$set(key, list())
}
.metadata[[key]][[metaKey]] <<- value
m <- .metadata$get(key)
m[[metaKey]] <- value
.metadata$set(key, m)
},
# Mark a value as frozen If accessed while frozen, a shiny.silent.error will
# be thrown.
freeze = function(key) {
domain <- getDefaultReactiveDomain()
# rLog$freezeReactiveKey(.reactId, key, domain)
setMeta(key, "frozen", TRUE)
},
thaw = function(key) {
domain <- getDefaultReactiveDomain()
# rLog$thawReactiveKey(.reactId, key, domain)
setMeta(key, "frozen", NULL)
},
@@ -401,19 +494,30 @@ ReactiveValues <- R6Class(
},
toList = function(all.names=FALSE) {
.graphDependsOn(.getReactiveEnvironment()$currentContext()$id,
sprintf('%s (all)', .label))
if (all.names)
listValue <- .values$values()
if (!all.names) {
listValue <- listValue[!grepl("^\\.", base::names(listValue))]
}
if (all.names) {
if (!isTRUE(.hasRetrieved$asListAll)) {
domain <- getDefaultReactiveDomain()
# rLog$defineAsListAll(.reactId, listValue, .label, domain)
.hasRetrieved$asListAll <<- TRUE
}
.allValuesDeps$register()
}
if (!isTRUE(.hasRetrieved$asList)) {
domain <- getDefaultReactiveDomain()
# making sure the value being recorded is with `all.names = FALSE`
# rLog$defineAsList(.reactId, listValue[!grepl("^\\.", base::names(listValue))], .label, domain)
.hasRetrieved$asList <<- TRUE
}
.valuesDeps$register()
return(as.list(.values, all.names=all.names))
},
.setLabel = function(label) {
.label <<- label
return(listValue)
}
)
)
@@ -562,11 +666,6 @@ as.list.reactivevalues <- function(x, all.names=FALSE, ...) {
reactiveValuesToList(x, all.names)
}
# For debug purposes
.setLabel <- function(x, label) {
.subset2(x, 'impl')$.setLabel(label)
}
#' Convert a reactivevalues object to a list
#'
#' This function does something similar to what you might \code{\link[base]{as.list}}
@@ -689,6 +788,7 @@ Observable <- R6Class(
'Observable',
portable = FALSE,
public = list(
.reactId = character(0),
.origFunc = 'function',
.func = 'function',
.label = character(0),
@@ -719,16 +819,18 @@ Observable <- R6Class(
funcLabel <- paste0("<reactive:", label, ">")
}
.reactId <<- nextGlobalReactId()
.origFunc <<- func
.func <<- wrapFunctionLabel(func, funcLabel,
..stacktraceon = ..stacktraceon)
.label <<- label
.domain <<- domain
.dependents <<- Dependents$new()
.dependents <<- Dependents$new(reactId = .reactId)
.invalidated <<- TRUE
.running <<- FALSE
.execCount <<- 0L
.mostRecentCtxId <<- ""
# rLog$define(.reactId, .value, .label, type = "observable", .domain)
},
getValue = function() {
.dependents$register()
@@ -739,8 +841,6 @@ Observable <- R6Class(
)
}
.graphDependsOnId(getCurrentContext()$id, .mostRecentCtxId)
if (.error) {
stop(.value)
}
@@ -756,12 +856,12 @@ Observable <- R6Class(
},
.updateValue = function() {
ctx <- Context$new(.domain, .label, type = 'observable',
prevId = .mostRecentCtxId)
prevId = .mostRecentCtxId, reactId = .reactId)
.mostRecentCtxId <<- ctx$id
ctx$onInvalidate(function() {
.invalidated <<- TRUE
.value <<- NULL # Value can be GC'd, it won't be read once invalidated
.dependents$invalidate()
.dependents$invalidate(log = FALSE)
})
.execCount <<- .execCount + 1L
@@ -935,6 +1035,7 @@ Observer <- R6Class(
'Observer',
portable = FALSE,
public = list(
.reactId = character(0),
.func = 'function',
.label = character(0),
.domain = 'ANY',
@@ -978,11 +1079,14 @@ Observer <- R6Class(
.autoDestroyHandle <<- NULL
setAutoDestroy(autoDestroy)
.reactId <<- nextGlobalReactId()
# rLog$defineObserver(.reactId, .label, .domain)
# Defer the first running of this until flushReact is called
.createContext()$invalidate()
},
.createContext = function() {
ctx <- Context$new(.domain, .label, type='observer', prevId=.prevId)
ctx <- Context$new(.domain, .label, type='observer', prevId=.prevId, reactId = .reactId)
.prevId <<- ctx$id
if (!is.null(.ctx)) {
@@ -1393,6 +1497,10 @@ reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain())
# callback below is fired (see #1621).
force(session)
# TODO-barret - ## leave alone for now
# reactId <- nextGlobalReactId()
# rLog$define(reactId, paste0("timer(", intervalMs, ")"))
dependents <- Map$new()
timerHandle <- scheduleTask(intervalMs, function() {
# Quit if the session is closed
@@ -1402,14 +1510,23 @@ reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain())
timerHandle <<- scheduleTask(intervalMs, sys.function())
session$cycleStartAction(function() {
doInvalidate <- function() {
lapply(
dependents$values(),
function(dep.ctx) {
dep.ctx$invalidate()
NULL
})
})
}
if (!is.null(session)) {
# If this timer belongs to a session, we must wait until the next cycle is
# ready to invalidate.
session$cycleStartAction(doInvalidate)
} else {
# If this timer doesn't belong to a session, we invalidate right away.
doInvalidate()
}
})
if (!is.null(session)) {
@@ -1417,14 +1534,15 @@ reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain())
}
return(function() {
ctx <- .getReactiveEnvironment()$currentContext()
newValue <- Sys.time()
ctx <- getCurrentContext()
if (!dependents$containsKey(ctx$id)) {
dependents$set(ctx$id, ctx)
ctx$onInvalidate(function() {
dependents$remove(ctx$id)
})
}
return(Sys.time())
return(newValue)
})
}
@@ -1483,8 +1601,12 @@ reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain())
#' }
#' @export
invalidateLater <- function(millis, session = getDefaultReactiveDomain()) {
force(session)
ctx <- .getReactiveEnvironment()$currentContext()
ctx <- getCurrentContext()
# rLog$invalidateLater(ctx$.reactId, ctx$id, millis, session)
timerHandle <- scheduleTask(millis, function() {
if (is.null(session)) {
ctx$invalidate()
@@ -1749,7 +1871,12 @@ reactiveFileReader <- function(intervalMillis, session, filePath, readFunc, ...)
#' # input object, like input$x
#' @export
isolate <- function(expr) {
ctx <- Context$new(getDefaultReactiveDomain(), '[isolate]', type='isolate')
if (hasCurrentContext()) {
reactId <- getCurrentContext()$.reactId
} else {
reactId <- rLog$noReactId
}
ctx <- Context$new(getDefaultReactiveDomain(), '[isolate]', type='isolate', reactId = reactId)
on.exit(ctx$invalidate())
# Matching ..stacktraceon../..stacktraceoff.. pair
..stacktraceoff..(ctx$run(function() {

View File

@@ -476,62 +476,64 @@ renderCachedPlot <- function(expr,
}
)
},
function(result) {
width <- result$width
height <- result$height
pixelratio <- result$pixelratio
function(possiblyAsyncResult) {
hybrid_chain(possiblyAsyncResult, function(result) {
width <- result$width
height <- result$height
pixelratio <- result$pixelratio
# Three possibilities when we get here:
# 1. There was a cache hit. No need to set a value in the cache.
# 2. There was a cache miss, and the plotObj is already the correct
# size (because drawReactive re-executed). In this case, we need
# to cache it.
# 3. There was a cache miss, and the plotObj was not the corect size.
# In this case, we need to replay the display list, and then cache
# the result.
if (!result$cacheHit) {
# If the image is already the correct size, this just returns the
# object unchanged.
result$plotObj <- do.call("resizeSavedPlot", c(
list(
name,
shinysession,
result$plotObj,
width,
height,
pixelratio,
res
),
args
))
# 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)
}
# 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 <- 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
img
})
}
)
}

View File

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

View File

@@ -1,7 +1,9 @@
#' @include server-input-handlers.R
appsByToken <- Map$new()
appsNeedingFlush <- Map$new()
# These Map objects are initialized in .onLoad() because Maps based on fastmap
# can't be saved in one R session and loaded in another.
appsByToken <- NULL
appsNeedingFlush <- NULL
# Provide a character representation of the WS that can be used
# as a key in a Map.
@@ -22,6 +24,7 @@ registerClient <- function(client) {
}
.globals$resourcePaths <- list()
.globals$resources <- list()
.globals$showcaseDefault <- 0
@@ -41,11 +44,6 @@ registerClient <- function(client) {
#' @param directoryPath The directory that contains the static resources to be
#' served.
#'
#' @details You can call \code{addResourcePath} multiple times for a given
#' \code{prefix}; only the most recent value will be retained. If the
#' normalized \code{directoryPath} is different than the directory that's
#' currently mapped to the \code{prefix}, a warning will be issued.
#'
#' @seealso \code{\link{singleton}}
#'
#' @examples
@@ -66,30 +64,65 @@ addResourcePath <- function(prefix, directoryPath) {
"`prefix` = '", prefix, "'; `directoryPath` = '" , directoryPath, "'")
}
)
# If a shiny app is currently running, dynamically register this path with
# the corresponding httpuv server object.
if (!is.null(getShinyOption("server")))
{
getShinyOption("server")$setStaticPath(.list = stats::setNames(normalizedPath, prefix))
}
# .globals$resourcePaths and .globals$resources persist across runs of applications.
.globals$resourcePaths[[prefix]] <- staticPath(normalizedPath)
# This is necessary because resourcePaths is only for serving assets out of C++;
# to support subapps, we also need assets to be served out of R, because those
# URLs are rewritten by R code (i.e. routeHandler) before they can be matched to
# a resource path.
.globals$resources[[prefix]] <- list(
directoryPath = normalizedPath,
func = staticHandler(normalizedPath)
)
}
# This function handles any GET request with two or more path elements where the
# first path element matches a prefix that was previously added using
# addResourcePath().
#
# For example, if `addResourcePath("foo", "~/bar")` was called, then a GET
# request for /foo/one/two.html would rewrite the PATH_INFO as /one/two.html and
# send it to the resource path function for "foo". As of this writing, that
# function will always be a staticHandler, which serves up a file if it exists
# and NULL if it does not.
#
# Since Shiny 1.3.x, assets registered via addResourcePath should mostly be
# served out of httpuv's native static file serving features. However, in the
# specific case of subapps, the R code path must be used, because subapps insert
# a giant random ID into the beginning of the URL that must be stripped off by
# an R route handler (see addSubApp()).
resourcePathHandler <- function(req) {
if (!identical(req$REQUEST_METHOD, 'GET'))
return(NULL)
# e.g. "/foo/one/two.html"
path <- req$PATH_INFO
match <- regexpr('^/([^/]+)/', path, perl=TRUE)
if (match == -1)
return(NULL)
len <- attr(match, 'capture.length')
# e.g. "foo"
prefix <- substr(path, 2, 2 + len - 1)
resInfo <- .globals$resources[[prefix]]
if (is.null(resInfo))
return(NULL)
# e.g. "/one/two.html"
suffix <- substr(path, 2 + len, nchar(path))
# Create a new request that's a clone of the current request, but adjust
# PATH_INFO and SCRIPT_NAME to reflect that we have already matched the first
# path element (e.g. "/foo"). See routeHandler() for more info.
subreq <- as.environment(as.list(req, all.names=TRUE))
subreq$PATH_INFO <- suffix
subreq$SCRIPT_NAME <- paste(subreq$SCRIPT_NAME, substr(path, 1, 2 + len), sep='')
@@ -187,7 +220,7 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
# This value, if non-NULL, must be present on all HTTP and WebSocket
# requests as the Shiny-Shared-Secret header or else access will be
# denied (403 response for HTTP, and instant close for websocket).
sharedSecret <- getOption('shiny.sharedSecret')
checkSharedSecret <- loadSharedSecret()
appHandlers <- list(
http = joinHandlers(c(
@@ -195,10 +228,10 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
httpHandlers,
sys.www.root,
resourcePathHandler,
reactLogHandler)),
reactLogHandler
)),
ws = function(ws) {
if (!is.null(sharedSecret)
&& !identical(sharedSecret, ws$request$HTTP_SHINY_SHARED_SECRET)) {
if (!checkSharedSecret(ws$request$HTTP_SHINY_SHARED_SECRET)) {
ws$close()
return(TRUE)
}
@@ -417,6 +450,27 @@ startApp <- function(appObj, port, host, quiet) {
handlerManager$addHandler(appHandlers$http, "/", tail = TRUE)
handlerManager$addWSHandler(appHandlers$ws, "/", tail = TRUE)
httpuvApp <- handlerManager$createHttpuvApp()
httpuvApp$staticPaths <- c(
appObj$staticPaths,
list(
# Always handle /session URLs dynamically, even if / is a static path.
"session" = excludeStaticPath(),
"shared" = system.file(package = "shiny", "www", "shared")
),
.globals$resourcePaths
)
httpuvApp$staticPathOptions <- httpuv::staticPathOptions(
html_charset = "utf-8",
headers = list("X-UA-Compatible" = "IE=edge,chrome=1"),
validation =
if (!is.null(getOption("shiny.sharedSecret"))) {
sprintf('"Shiny-Shared-Secret" == "%s"', getOption("shiny.sharedSecret"))
} else {
character(0)
}
)
if (is.numeric(port) || is.integer(port)) {
if (!quiet) {
hostString <- host
@@ -424,7 +478,7 @@ startApp <- function(appObj, port, host, quiet) {
hostString <- paste0("[", hostString, "]")
message('\n', 'Listening on http://', hostString, ':', port)
}
return(startServer(host, port, handlerManager$createHttpuvApp()))
return(startServer(host, port, httpuvApp))
} else if (is.character(port)) {
if (!quiet) {
message('\n', 'Listening on domain socket ', port)
@@ -436,7 +490,7 @@ startApp <- function(appObj, port, host, quiet) {
"configuration (and not domain sockets), then `port` must ",
"be numeric, not a string.")
}
return(startPipeServer(port, mask, handlerManager$createHttpuvApp()))
return(startPipeServer(port, mask, httpuvApp))
}
}
@@ -777,6 +831,10 @@ runApp <- function(appDir=getwd(),
server <- startApp(appParts, port, host, quiet)
# Make the httpuv server object accessible. Needed for calling
# addResourcePath while app is running.
shinyOptions(server = server)
on.exit({
stopServer(server)
}, add = TRUE)

View File

@@ -62,7 +62,7 @@ NULL
#' by setting e.g. \code{options(shiny.autoreload.interval = 2000)} (every
#' two seconds).}
#' \item{shiny.reactlog}{If \code{TRUE}, enable logging of reactive events,
#' which can be viewed later with the \code{\link{showReactLog}} function.
#' which can be viewed later with the \code{\link{reactlogShow}} function.
#' This incurs a substantial performance penalty and should not be used in
#' production.}
#' \item{shiny.usecairo}{This is used to disable graphical rendering by the
@@ -513,8 +513,7 @@ ShinySession <- R6Class(
# in the web page; in these cases, there's no output_foo_hidden flag,
# and hidden should be TRUE. In other words, NULL and TRUE should map to
# TRUE, FALSE should map to FALSE.
hidden <- private$.clientData$.values[[paste("output_", name, "_hidden",
sep="")]]
hidden <- private$.clientData$.values$get(paste0("output_", name, "_hidden"))
if (is.null(hidden)) hidden <- TRUE
return(hidden && private$getOutputOption(name, 'suspendWhenHidden', TRUE))
@@ -576,7 +575,7 @@ ShinySession <- R6Class(
# Apply preprocessor functions for inputs that have them.
values$input <- lapply(
setNames(names(values$input), names(values$input)),
stats::setNames(names(values$input), names(values$input)),
function(name) {
preprocess <- private$getSnapshotPreprocessInput(name)
preprocess(values$input[[name]])
@@ -604,7 +603,7 @@ ShinySession <- R6Class(
# Apply snapshotPreprocess functions for outputs that have them.
values$output <- lapply(
setNames(names(values$output), names(values$output)),
stats::setNames(names(values$output), names(values$output)),
function(name) {
preprocess <- private$getSnapshotPreprocessOutput(name)
preprocess(values$output[[name]])
@@ -683,11 +682,42 @@ ShinySession <- R6Class(
# See cycleStartAction
startCycle = function() {
# TODO: This should check for busyCount == 0L, and remove the checks from
# the call sites
if (length(private$cycleStartActionQueue) > 0) {
head <- private$cycleStartActionQueue[[1L]]
private$cycleStartActionQueue <- private$cycleStartActionQueue[-1L]
# After we execute the current cycleStartAction (head), there may be
# more items left on the queue. If the current busyCount > 0, then that
# means an async task is running; whenever that task finishes, it will
# decrement the busyCount back to 0 and a startCycle will then be
# scheduled. But if the current busyCount is 0, it means that either
# busyCount was incremented and then decremented; OR that running head()
# never touched busyCount (one example of the latter is that an input
# changed that didn't actually cause any observers to be invalidated,
# i.e. an input that's used in the body of an observeEvent). Because of
# the possibility of the latter case, we need to conditionally schedule
# a startCycle ourselves to ensure that the remaining queue items get
# processed.
#
# Since we can't actually tell whether head() increment and decremented
# busyCount, it's possible we're calling startCycle spuriously; that's
# OK, it's essentially a no-op in that case.
on.exit({
if (private$busyCount == 0L && length(private$cycleStartActionQueue) > 0L) {
later::later(function() {
if (private$busyCount == 0L) {
private$startCycle()
}
})
}
}, add = TRUE)
head()
}
invisible()
}
),
public = list(
@@ -719,8 +749,8 @@ ShinySession <- R6Class(
private$flushCallbacks <- Callbacks$new()
private$flushedCallbacks <- Callbacks$new()
private$inputReceivedCallbacks <- Callbacks$new()
private$.input <- ReactiveValues$new(dedupe = FALSE)
private$.clientData <- ReactiveValues$new(dedupe = TRUE)
private$.input <- ReactiveValues$new(dedupe = FALSE, label = "input")
private$.clientData <- ReactiveValues$new(dedupe = TRUE, label = "clientData")
private$timingRecorder <- ShinyServerTimingRecorder$new()
self$progressStack <- Stack$new()
self$files <- Map$new()
@@ -728,9 +758,7 @@ ShinySession <- R6Class(
self$userData <- new.env(parent = emptyenv())
self$input <- .createReactiveValues(private$.input, readonly=TRUE)
.setLabel(self$input, 'input')
self$clientData <- .createReactiveValues(private$.clientData, readonly=TRUE)
.setLabel(self$clientData, 'clientData')
self$output <- .createOutputWriter(self)
@@ -1175,6 +1203,11 @@ ShinySession <- R6Class(
if (self$isClosed())
return()
# This is the only place in the session where the restoreContext is
# flushed.
if (!is.null(self$restoreContext))
self$restoreContext$flushPending()
# Return TRUE if there's any stuff to send to the client.
hasPendingUpdates <- function() {
# Even though progressKeys isn't sent to the client, we use it in this
@@ -1883,7 +1916,7 @@ ShinySession <- R6Class(
fileData <- readBin(file, 'raw', n=bytes)
if (isTRUE(private$.clientData$.values$allowDataUriScheme)) {
if (isTRUE(private$.clientData$.values$get("allowDataUriScheme"))) {
b64 <- rawToBase64(fileData)
return(paste('data:', contentType, ';base64,', b64, sep=''))
} else {
@@ -1994,6 +2027,7 @@ ShinySession <- R6Class(
},
incrementBusyCount = function() {
if (private$busyCount == 0L) {
rLog$asyncStart(domain = self)
private$sendMessage(busy = "busy")
}
private$busyCount <- private$busyCount + 1L
@@ -2001,6 +2035,7 @@ ShinySession <- R6Class(
decrementBusyCount = function() {
private$busyCount <- private$busyCount - 1L
if (private$busyCount == 0L) {
rLog$asyncStop(domain = self)
private$sendMessage(busy = "idle")
self$requestFlush()
# We defer the call to startCycle() using later(), to defend against
@@ -2184,7 +2219,8 @@ flushPendingSessions <- function() {
})
}
.globals$onStopCallbacks <- Callbacks$new()
# Initialized in .onLoad
.globals$onStopCallbacks <- NULL
#' Run code after an application or session ends
#'

View File

@@ -86,7 +86,8 @@ TimerCallbacks <- R6Class(
)
)
timerCallbacks <- TimerCallbacks$new()
# Initialized in onLoad() because TimerCallbacks uses Map.
timerCallbacks <- NULL
scheduleTask <- function(millis, callback) {
cancelled <- FALSE

View File

@@ -205,18 +205,9 @@ updateActionButton <- function(session, inputId, label = NULL, icon = NULL) {
updateDateInput <- function(session, inputId, label = NULL, value = NULL,
min = NULL, max = NULL) {
# Make sure values are NULL or Date objects. This is so we can ensure that
# they will be formatted correctly. For example, the string "2016-08-9" is not
# correctly formatted, but the conversion to Date and back to string will fix
# it.
formatDate <- function(x) {
if (is.null(x))
return(NULL)
format(as.Date(x), "%Y-%m-%d")
}
value <- formatDate(value)
min <- formatDate(min)
max <- formatDate(max)
value <- dateYMD(value, "value")
min <- dateYMD(min, "min")
max <- dateYMD(max, "max")
message <- dropNulls(list(label=label, value=value, min=min, max=max))
session$sendInputMessage(inputId, message)
@@ -266,12 +257,11 @@ updateDateInput <- function(session, inputId, label = NULL, value = NULL,
updateDateRangeInput <- function(session, inputId, label = NULL,
start = NULL, end = NULL, min = NULL,
max = NULL) {
# Make sure start and end are strings, not date objects. This is for
# consistency across different locales.
if (inherits(start, "Date")) start <- format(start, '%Y-%m-%d')
if (inherits(end, "Date")) end <- format(end, '%Y-%m-%d')
if (inherits(min, "Date")) min <- format(min, '%Y-%m-%d')
if (inherits(max, "Date")) max <- format(max, '%Y-%m-%d')
start <- dateYMD(start, "start")
end <- dateYMD(end, "end")
min <- dateYMD(min, "min")
max <- dateYMD(max, "max")
message <- dropNulls(list(
label = label,
@@ -428,13 +418,15 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
min = NULL, max = NULL, step = NULL, timeFormat = NULL, timezone = NULL)
{
# If no min/max/value is provided, we won't know the
# type, and this will return an empty string
dataType <- getSliderType(min, max, value)
if (is.null(timeFormat)) {
timeFormat <- switch(dataType, date = "%F", datetime = "%F %T", number = NULL)
}
if (dataType == "date" || dataType == "datetime") {
if (isTRUE(dataType %in% c("date", "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)

View File

@@ -121,8 +121,8 @@ isWholeNum <- function(x, tol = .Machine$double.eps^0.5) {
}
`%AND%` <- function(x, y) {
if (!is.null(x) && !is.na(x))
if (!is.null(y) && !is.na(y))
if (!is.null(x) && !isTRUE(is.na(x)))
if (!is.null(y) && !isTRUE(is.na(y)))
return(y)
return(NULL)
}
@@ -1050,7 +1050,7 @@ safeError <- function(error) {
# #' @examples
# #' ## Note: the breaking of the reactive chain that happens in the app
# #' ## below (when input$txt = 'bad' and input$allowBad = 'FALSE') is
# #' ## easily visualized with `showReactLog()`
# #' ## easily visualized with `reactlogShow()`
# #'
# #' ## Only run examples in interactive R sessions
# #' if (interactive()) {
@@ -1560,6 +1560,25 @@ URLencode <- function(value, reserved = FALSE) {
if (reserved) encodeURIComponent(value) else encodeURI(value)
}
# Make user-supplied dates are either NULL or can be coerced
# to a yyyy-mm-dd formatted string. If a date is specified, this
# function returns a string for consistency across locales.
# Also, `as.Date()` is used to coerce strings to date objects
# so that strings like "2016-08-9" are expanded to "2016-08-09"
dateYMD <- function(date = NULL, argName = "value") {
if (!length(date)) return(NULL)
if (length(date) > 1) warning("Expected `", argName, "` to be of length 1.")
tryCatch(date <- format(as.Date(date), "%Y-%m-%d"),
error = function(e) {
warning(
"Couldn't coerce the `", argName,
"` argument to a date string with format yyyy-mm-dd",
call. = FALSE
)
}
)
date
}
# This function takes a name and function, and it wraps that function in a new
# function which calls the original function using the specified name. This can
@@ -1730,6 +1749,7 @@ createVarPromiseDomain <- function(env, name, value) {
getSliderType <- function(min, max, value) {
vals <- dropNulls(list(value, min, max))
if (length(vals) == 0) return("")
type <- unique(lapply(vals, function(x) {
if (inherits(x, "Date")) "date"
else if (inherits(x, "POSIXt")) "datetime"
@@ -1740,3 +1760,42 @@ getSliderType <- function(min, max, value) {
}
type[[1]]
}
# Reads the `shiny.sharedSecret` global option, and returns a function that can
# be used to test header values for a match.
loadSharedSecret <- function() {
normalizeToRaw <- function(value, label = "value") {
if (is.null(value)) {
raw()
} else if (is.character(value)) {
charToRaw(paste(value, collapse = "\n"))
} else if (is.raw(value)) {
value
} else {
stop("Wrong type for ", label, "; character or raw expected")
}
}
sharedSecret <- normalizeToRaw(getOption("shiny.sharedSecret"))
if (is.null(sharedSecret)) {
function(x) TRUE
} else {
# We compare the digest of the two values so that their lengths are equalized
function(x) {
x <- normalizeToRaw(x)
# Constant time comparison to avoid timing attacks
constantTimeEquals(sharedSecret, x)
}
}
}
# Compares two raw vectors of equal length for equality, in constant time
constantTimeEquals <- function(raw1, raw2) {
stopifnot(is.raw(raw1))
stopifnot(is.raw(raw2))
if (length(raw1) != length(raw2)) {
return(FALSE)
}
sum(as.integer(xor(raw1, raw2))) == 0
}

View File

@@ -65,7 +65,4 @@ We welcome contributions to the **shiny** package. Please see our [CONTRIBUTING.
## License
The shiny package is licensed under the GPLv3. See these files in the inst directory for additional details:
- COPYING - shiny package license (GPLv3)
- NOTICE - Copyright notices for additional included software
The shiny package as a whole is licensed under the GPLv3. See the [LICENSE](LICENSE) file for more details.

View File

@@ -134,7 +134,7 @@ sd_section("Reactive programming",
"isolate",
"invalidateLater",
"debounce",
"showReactLog",
"reactlog",
"makeReactiveBinding",
"reactiveFileReader",
"reactivePoll",

File diff suppressed because it is too large Load Diff

View File

@@ -529,7 +529,17 @@
},
_utc_to_local: function(utc){
return utc && new Date(utc.getTime() + (utc.getTimezoneOffset()*60000));
if (!utc) return utc;
var local = new Date(utc.getTime() + (utc.getTimezoneOffset() * 60000));
if (local.getTimezoneOffset() != utc.getTimezoneOffset())
{
local = new Date(utc.getTime() + (local.getTimezoneOffset() * 60000));
}
return utc && local;
},
_local_to_utc: function(local){
return local && new Date(local.getTime() - (local.getTimezoneOffset()*60000));
@@ -661,7 +671,7 @@
visualPadding = 10,
container = $(this.o.container),
windowWidth = container.width(),
scrollTop = this.o.container === 'body' ? $(document).scrollTop() : container.scrollTop(),
scrollTop = this.o.container === 'body:first' ? $(document).scrollTop() : container.scrollTop(),
appendOffset = container.offset();
var parentsZindex = [];
@@ -676,7 +686,7 @@
var left = offset.left - appendOffset.left,
top = offset.top - appendOffset.top;
if (this.o.container !== 'body') {
if (this.o.container !== 'body:first') {
top += scrollTop;
}
@@ -1756,7 +1766,7 @@
enableOnReadonly: true,
showOnFocus: true,
zIndexOffset: 10,
container: 'body',
container: 'body:first',
immediateUpdates: false,
title: '',
templates: {

File diff suppressed because one or more lines are too long

View File

@@ -12,6 +12,13 @@ pre.shiny-text-output.noplaceholder:empty {
height: 0;
}
/* Some browsers (like Safari) will wrap text in <pre> tags with Bootstrap's
CSS. This changes the behavior to not wrap.
*/
pre.shiny-text-output {
word-wrap: normal;
}
.shiny-image-output img.shiny-scalable, .shiny-plot-output img.shiny-scalable {
max-width: 100%;
max-height: 100%;
@@ -209,6 +216,10 @@ pre.shiny-text-output.noplaceholder:empty {
font-size: 80%;
}
.shiny-label-null {
display: none;
}
.crosshair {
cursor: crosshair;
}

View File

@@ -12,7 +12,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var exports = window.Shiny = window.Shiny || {};
exports.version = "1.2.0"; // Version number inserted by Grunt
exports.version = "1.3.2.9000"; // Version number inserted by Grunt
var origPushState = window.history.pushState;
window.history.pushState = function () {
@@ -321,6 +321,24 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
if (op === "==") return diff === 0;else if (op === ">=") return diff >= 0;else if (op === ">") return diff > 0;else if (op === "<=") return diff <= 0;else if (op === "<") return diff < 0;else throw "Unknown operator: " + op;
};
function updateLabel(labelTxt, labelNode) {
// Only update if label was specified in the update method
if (typeof labelTxt === "undefined") return;
if (labelNode.length !== 1) {
throw new Error("labelNode must be of length 1");
}
// Should the label be empty?
var emptyLabel = $.isArray(labelTxt) && labelTxt.length === 0;
if (emptyLabel) {
labelNode.addClass("shiny-label-null");
} else {
labelNode.text(labelTxt);
labelNode.removeClass("shiny-label-null");
}
}
//---------------------------------------------------------------------
// Source file: ../srcjs/browser.js
@@ -545,8 +563,8 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.lastChanceCallback = [];
};
(function () {
this.setInput = function (name, value, opts) {
this.pendingData[name] = value;
this.setInput = function (nameType, value, opts) {
this.pendingData[nameType] = value;
if (!this.reentrant) {
if (opts.priority === "event") {
@@ -582,8 +600,8 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.lastSentValues = this.reset(initialValues);
};
(function () {
this.setInput = function (name, value, opts) {
var _splitInputNameType = splitInputNameType(name);
this.setInput = function (nameType, value, opts) {
var _splitInputNameType = splitInputNameType(nameType);
var inputName = _splitInputNameType.name;
var inputType = _splitInputNameType.inputType;
@@ -610,10 +628,10 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
if (values.hasOwnProperty(inputName)) {
var _splitInputNameType2 = splitInputNameType(inputName);
var name = _splitInputNameType2.name;
var _name = _splitInputNameType2.name;
var inputType = _splitInputNameType2.inputType;
cacheValues[name] = {
cacheValues[_name] = {
jsonValue: JSON.stringify(values[inputName]),
inputType: inputType
};
@@ -628,10 +646,10 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.target = target;
};
(function () {
this.setInput = function (name, value, opts) {
this.setInput = function (nameType, value, opts) {
var evt = jQuery.Event("shiny:inputchanged");
var input = splitInputNameType(name);
var input = splitInputNameType(nameType);
evt.name = input.name;
evt.inputType = input.inputType;
evt.value = value;
@@ -657,25 +675,41 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.inputRatePolicies = {};
};
(function () {
this.setInput = function (name, value, opts) {
this.$ensureInit(name);
// Note that the first argument of setInput() and setRatePolicy()
// are passed both the input name (i.e., inputId) and type.
// https://github.com/rstudio/shiny/blob/67d3a/srcjs/init_shiny.js#L111-L126
// However, $ensureInit() and $doSetInput() are meant to be passed just
// the input name (i.e., inputId), which is why we distinguish between
// nameType and name.
this.setInput = function (nameType, value, opts) {
var _splitInputNameType3 = splitInputNameType(nameType);
if (opts.priority !== "deferred") this.inputRatePolicies[name].immediateCall(name, value, opts);else this.inputRatePolicies[name].normalCall(name, value, opts);
var inputName = _splitInputNameType3.name;
this.$ensureInit(inputName);
if (opts.priority !== "deferred") this.inputRatePolicies[inputName].immediateCall(nameType, value, opts);else this.inputRatePolicies[inputName].normalCall(nameType, value, opts);
};
this.setRatePolicy = function (name, mode, millis) {
this.setRatePolicy = function (nameType, mode, millis) {
var _splitInputNameType4 = splitInputNameType(nameType);
var inputName = _splitInputNameType4.name;
if (mode === 'direct') {
this.inputRatePolicies[name] = new Invoker(this, this.$doSetInput);
this.inputRatePolicies[inputName] = new Invoker(this, this.$doSetInput);
} else if (mode === 'debounce') {
this.inputRatePolicies[name] = new Debouncer(this, this.$doSetInput, millis);
this.inputRatePolicies[inputName] = new Debouncer(this, this.$doSetInput, millis);
} else if (mode === 'throttle') {
this.inputRatePolicies[name] = new Throttler(this, this.$doSetInput, millis);
this.inputRatePolicies[inputName] = new Throttler(this, this.$doSetInput, millis);
}
};
this.$ensureInit = function (name) {
if (!(name in this.inputRatePolicies)) this.setRatePolicy(name, 'direct');
};
this.$doSetInput = function (name, value, opts) {
this.target.setInput(name, value, opts);
this.$doSetInput = function (nameType, value, opts) {
this.target.setInput(nameType, value, opts);
};
}).call(InputRateDecorator.prototype);
@@ -684,8 +718,8 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.pendingInput = {};
};
(function () {
this.setInput = function (name, value, opts) {
if (/^\./.test(name)) this.target.setInput(name, value, opts);else this.pendingInput[name] = { value: value, opts: opts };
this.setInput = function (nameType, value, opts) {
if (/^\./.test(nameType)) this.target.setInput(nameType, value, opts);else this.pendingInput[name] = { value: value, opts: opts };
};
this.submit = function () {
for (var name in this.pendingInput) {
@@ -701,12 +735,12 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.target = target;
};
(function () {
this.setInput = function (name, value, opts) {
if (!name) throw "Can't set input with empty name.";
this.setInput = function (nameType, value, opts) {
if (!nameType) throw "Can't set input with empty name.";
opts = addDefaultInputOpts(opts);
this.target.setInput(name, value, opts);
this.target.setInput(nameType, value, opts);
};
}).call(InputValidateDecorator.prototype);
@@ -733,8 +767,8 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
return opts;
}
function splitInputNameType(name) {
var name2 = name.split(':');
function splitInputNameType(nameType) {
var name2 = nameType.split(':');
return {
name: name2[0],
inputType: name2.length > 1 ? name2[1] : ''
@@ -1771,7 +1805,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var $container = $('.shiny-progress-container');
if ($container.length === 0) {
$container = $('<div class="shiny-progress-container"></div>');
$('body').append($container);
$(document.body).append($container);
}
// Add div for just this progress ID
@@ -2025,7 +2059,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
if ($panel.length > 0) return $panel;
$('body').append('<div id="shiny-notification-panel">');
$(document.body).append('<div id="shiny-notification-panel">');
return $panel;
}
@@ -2105,7 +2139,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var $modal = $('#shiny-modal-wrapper');
if ($modal.length === 0) {
$modal = $('<div id="shiny-modal-wrapper"></div>');
$('body').append($modal);
$(document.body).append($modal);
// If the wrapper's content is a Bootstrap modal, then when the inner
// modal is hidden, remove the entire thing, including wrapper.
@@ -4326,7 +4360,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
receiveMessage: function receiveMessage(el, data) {
if (data.hasOwnProperty('value')) this.setValue(el, data.value);
if (data.hasOwnProperty('label')) $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
if (data.hasOwnProperty('placeholder')) el.placeholder = data.placeholder;
@@ -4334,7 +4368,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
},
getState: function getState(el) {
return {
label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
label: this._getLabelNode(el).text(),
value: el.value,
placeholder: el.placeholder
};
@@ -4344,6 +4378,9 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
policy: 'debounce',
delay: 250
};
},
_getLabelNode: function _getLabelNode(el) {
return $(el).parent().find('label[for="' + $escape(el.id) + '"]');
}
});
inputBindings.register(textInputBinding, 'shiny.textInput');
@@ -4399,16 +4436,19 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
if (data.hasOwnProperty('max')) el.max = data.max;
if (data.hasOwnProperty('step')) el.step = data.step;
if (data.hasOwnProperty('label')) $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
$(el).trigger('change');
},
getState: function getState(el) {
return { label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
return { label: this._getLabelNode(el).text(),
value: this.getValue(el),
min: Number(el.min),
max: Number(el.max),
step: Number(el.step) };
},
_getLabelNode: function _getLabelNode(el) {
return $(el).parent().find('label[for="' + $escape(el.id) + '"]');
}
});
inputBindings.register(numberInputBinding, 'shiny.numberInput');
@@ -4444,6 +4484,8 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
receiveMessage: function receiveMessage(el, data) {
if (data.hasOwnProperty('value')) el.checked = data.value;
// checkboxInput()'s label works different from other
// input labels...the label container should always exist
if (data.hasOwnProperty('label')) $(el).parent().find('span').text(data.label);
$(el).trigger('change');
@@ -4572,7 +4614,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
}
}
if (data.hasOwnProperty('label')) $el.parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
var domElements = ['data-type', 'time-format', 'timezone'];
for (var i = 0; i < domElements.length; i++) {
@@ -4614,7 +4656,9 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
$el.ionRangeSlider(opts);
},
_getLabelNode: function _getLabelNode(el) {
return $(el).parent().find('label[for="' + $escape(el.id) + '"]');
},
// Number of values; 1 for single slider, 2 for range slider
_numValues: function _numValues(el) {
if ($(el).data('ionRangeSlider').options.type === 'double') return 2;else return 1;
@@ -4776,7 +4820,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
if (startview === 2) startview = 'decade';else if (startview === 1) startview = 'year';else if (startview === 0) startview = 'month';
return {
label: $el.find('label[for="' + $escape(el.id) + '"]').text(),
label: this._getLabelNode(el).text(),
value: this.getValue(el),
valueString: $input.val(),
min: min,
@@ -4790,7 +4834,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
receiveMessage: function receiveMessage(el, data) {
var $input = $(el).find('input');
if (data.hasOwnProperty('label')) $(el).find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
if (data.hasOwnProperty('min')) this._setMin($input[0], data.min);
@@ -4845,6 +4889,9 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this._setMax($input[0], $input.data('max-date'));
}
},
_getLabelNode: function _getLabelNode(el) {
return $(el).find('label[for="' + $escape(el.id) + '"]');
},
// Given a format object from a date picker, return a string
_formatToString: function _formatToString(format) {
// Format object has structure like:
@@ -4991,7 +5038,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
if (startview === 2) startview = 'decade';else if (startview === 1) startview = 'year';else if (startview === 0) startview = 'month';
return {
label: $el.find('label[for="' + $escape(el.id) + '"]').text(),
label: this._getLabelNode(el).text(),
value: this.getValue(el),
valueString: [$startinput.val(), $endinput.val()],
min: min,
@@ -5008,7 +5055,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var $startinput = $inputs.eq(0);
var $endinput = $inputs.eq(1);
if (data.hasOwnProperty('label')) $el.find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
if (data.hasOwnProperty('min')) {
this._setMin($startinput[0], data.min);
@@ -5064,6 +5111,9 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
},
unsubscribe: function unsubscribe(el) {
$(el).off('.dateRangeInputBinding');
},
_getLabelNode: function _getLabelNode(el) {
return $(el).find('label[for="' + $escape(el.id) + '"]');
}
});
inputBindings.register(dateRangeInputBinding, 'shiny.dateRangeInput');
@@ -5095,10 +5145,14 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
return $(el).val();
},
setValue: function setValue(el, value) {
var selectize = this._selectize(el);
if (typeof selectize !== 'undefined') {
selectize.setValue(value);
} else $(el).val(value);
if (!this._is_selectize(el)) {
$(el).val(value);
} else {
var selectize = this._selectize(el);
if (selectize) {
selectize.setValue(value);
}
}
},
getState: function getState(el) {
// Store options in an array of objects, each with with value and label
@@ -5109,7 +5163,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
}
return {
label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
label: this._getLabelNode(el),
value: this.getValue(el),
options: options
};
@@ -5191,7 +5245,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.setValue(el, data.value);
}
if (data.hasOwnProperty('label')) $(el).parent().parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
$(el).trigger('change');
},
@@ -5214,6 +5268,18 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
initialize: function initialize(el) {
this._selectize(el);
},
_getLabelNode: function _getLabelNode(el) {
var escaped_id = $escape(el.id);
if (this._is_selectize(el)) {
escaped_id += "-selectized";
}
return $(el).parent().parent().find('label[for="' + escaped_id + '"]');
},
// Return true if it's a selectize input, false if it's a regular select input.
_is_selectize: function _is_selectize(el) {
var config = $(el).parent().find('script[data-for="' + $escape(el.id) + '"]');
return config.length > 0;
},
_selectize: function _selectize(el, update) {
if (!$.fn.selectize) return undefined;
var $el = $(el);
@@ -5286,7 +5352,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
}
return {
label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
label: this._getLabelNode(el).text(),
value: this.getValue(el),
options: options
};
@@ -5305,7 +5371,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
if (data.hasOwnProperty('value')) this.setValue(el, data.value);
if (data.hasOwnProperty('label')) $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
$(el).trigger('change');
},
@@ -5317,6 +5383,10 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
unsubscribe: function unsubscribe(el) {
$(el).off('.radioInputBinding');
},
// Get the DOM element that contains the top-level label
_getLabelNode: function _getLabelNode(el) {
return $(el).parent().find('label[for="' + $escape(el.id) + '"]');
},
// Given an input DOM object, get the associated label. Handles labels
// that wrap the input as well as labels associated with 'for' attribute.
_getLabel: function _getLabel(obj) {
@@ -5382,7 +5452,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
label: this._getLabel($objs[i]) };
}
return { label: $(el).find('label[for="' + $escape(el.id) + '"]').text(),
return { label: this._getLabelNode(el).text(),
value: this.getValue(el),
options: options
};
@@ -5401,7 +5471,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
if (data.hasOwnProperty('value')) this.setValue(el, data.value);
if (data.hasOwnProperty('label')) $el.find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
$(el).trigger('change');
},
@@ -5413,6 +5483,10 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
unsubscribe: function unsubscribe(el) {
$(el).off('.checkboxGroupInputBinding');
},
// Get the DOM element that contains the top-level label
_getLabelNode: function _getLabelNode(el) {
return $(el).find('label[for="' + $escape(el.id) + '"]');
},
// Given an input DOM object, get the associated label. Handles labels
// that wrap the input as well as labels associated with 'for' attribute.
_getLabel: function _getLabel(obj) {
@@ -5578,7 +5652,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.iframe.id = iframeId;
this.iframe.name = iframeId;
this.iframe.setAttribute('style', 'position: fixed; top: 0; left: 0; width: 0; height: 0; border: none');
$('body').append(this.iframe);
$(document.body).append(this.iframe);
var iframeDestroy = function iframeDestroy() {
// Forces Shiny to flushReact, flush outputs, etc. Without this we get
// invalidated reactives, but observers don't actually execute.
@@ -6442,14 +6516,14 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
// Need to register callbacks for each Bootstrap 3 class.
var bs3classes = ['modal', 'dropdown', 'tab', 'tooltip', 'popover', 'collapse'];
$.each(bs3classes, function (idx, classname) {
$('body').on('shown.bs.' + classname + '.sendImageSize', '*', filterEventsByNamespace('bs', sendImageSize));
$('body').on('shown.bs.' + classname + '.sendOutputHiddenState ' + 'hidden.bs.' + classname + '.sendOutputHiddenState', '*', filterEventsByNamespace('bs', sendOutputHiddenState));
$(document.body).on('shown.bs.' + classname + '.sendImageSize', '*', filterEventsByNamespace('bs', sendImageSize));
$(document.body).on('shown.bs.' + classname + '.sendOutputHiddenState ' + 'hidden.bs.' + classname + '.sendOutputHiddenState', '*', filterEventsByNamespace('bs', sendOutputHiddenState));
});
// This is needed for Bootstrap 2 compatibility and for non-Bootstrap
// related shown/hidden events (like conditionalPanel)
$('body').on('shown.sendImageSize', '*', sendImageSize);
$('body').on('shown.sendOutputHiddenState hidden.sendOutputHiddenState', '*', sendOutputHiddenState);
$(document.body).on('shown.sendImageSize', '*', sendImageSize);
$(document.body).on('shown.sendOutputHiddenState hidden.sendOutputHiddenState', '*', sendOutputHiddenState);
// Send initial pixel ratio, and update it if it changes
initialValues['.clientdata_pixelratio'] = pixelRatio();
@@ -6547,6 +6621,25 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
e.preventDefault();
});
$(document).on('keydown', function (e) {
if (e.which !== 115 || !e.ctrlKey && !e.metaKey || e.shiftKey || e.altKey) return;
var url = 'reactlog/mark?w=' + window.escape(exports.shinyapp.config.workerId) + "&s=" + window.escape(exports.shinyapp.config.sessionId);
// send notification
$.get(url, function (result) {
if (result !== "marked") return;
var html = '<span id="shiny-reactlog-mark-text">Marked time point in reactlog</span>';
exports.notifications.show({
html: html,
closeButton: true
});
});
e.preventDefault();
});
//---------------------------------------------------------------------
// Source file: ../srcjs/_end.js
})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -20,12 +20,6 @@ Adds a directory of static resources to Shiny's web server, with the given
path prefix. Primarily intended for package authors to make supporting
JavaScript/CSS files available to their components.
}
\details{
You can call \code{addResourcePath} multiple times for a given
\code{prefix}; only the most recent value will be retained. If the
normalized \code{directoryPath} is different than the directory that's
currently mapped to the \code{prefix}, a warning will be issued.
}
\examples{
addResourcePath('datasets', system.file('data', package='datasets'))
}

View File

@@ -8,8 +8,8 @@ icon(name, class = NULL, lib = "font-awesome")
}
\arguments{
\item{name}{Name of icon. Icons are drawn from the
\href{https://fontawesome.com/}{Font Awesome} (currently icons from
the v5.3.1 set are supported) and
\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
@@ -30,10 +30,6 @@ of a button, or as an icon for a \code{\link{tabPanel}} within a
\code{\link{navbarPage}}.
}
\examples{
icon("calendar") # standard icon
icon("calendar", "fa-3x") # 3x normal size
icon("cog", lib = "glyphicon") # From glyphicon library
# add an icon to a submit button
submitButton("Update View", icon = icon("refresh"))

View File

@@ -10,7 +10,7 @@ reactiveVal(value = NULL, label = NULL)
\item{value}{An optional initial value.}
\item{label}{An optional label, for debugging purposes (see
\code{\link{showReactLog}}). If missing, a label will be automatically
\code{\link{reactlog}}). If missing, a label will be automatically
created.}
}
\value{

View File

@@ -1,14 +1,23 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/graph.R
\name{showReactLog}
\name{reactlog}
\alias{reactlog}
\alias{reactlogShow}
\alias{showReactLog}
\alias{reactlogReset}
\title{Reactive Log Visualizer}
\usage{
reactlog()
reactlogShow(time = TRUE)
showReactLog(time = TRUE)
reactlogReset()
}
\arguments{
\item{time}{A boolean that specifies whether or not to display the
time that each reactive.}
time that each reactive takes to calculate a result.}
}
\description{
Provides an interactive browser-based tool for visualizing reactive
@@ -32,14 +41,26 @@ in the process, not just for a particular application or session.
As an alternative to pressing Ctrl/Command+F3--for example, if you
are using reactives outside of the context of a Shiny
application--you can run the \code{showReactLog} function, which will
application--you can run the \code{reactlogShow} function, which will
generate the reactive log visualization as a static HTML file and
launch it in your default browser. In this case, refreshing your
browser will not load new activity into the report; you will need to
call \code{showReactLog()} explicitly.
call \code{reactlogShow()} explicitly.
For security and performance reasons, do not enable
\code{shiny.reactlog} in production environments. When the option is
enabled, it's possible for any user of your app to see at least some
of the source code of your reactive expressions and observers.
}
\section{Functions}{
\itemize{
\item \code{reactlog}: Return a list of reactive information. Can be used in conjunction with
\code{reactlog::\link[reactlog]{reactlog_show}} to later display the reactlog graph.
\item \code{reactlogShow}: Display a full reactlog graph for all sessions.
\item \code{showReactLog}: This function is deprecated. You should use \code{\link{reactlogShow}}
\item \code{reactlogReset}: Resets the entire reactlog stack. Useful for debugging and removing all prior reactive history.
}}

View File

@@ -56,7 +56,7 @@ from a list of values.
\details{
By default, \code{selectInput()} and \code{selectizeInput()} use the
JavaScript library \pkg{selectize.js}
(\url{https://github.com/selectize/selectize.js}) to instead of the basic
(\url{https://github.com/selectize/selectize.js}) instead of the basic
select input element. To use the standard HTML select input element, use
\code{selectInput()} with \code{selectize=FALSE}.

View File

@@ -36,7 +36,7 @@ The default polling interval is 500 milliseconds. You can change this
by setting e.g. \code{options(shiny.autoreload.interval = 2000)} (every
two seconds).}
\item{shiny.reactlog}{If \code{TRUE}, enable logging of reactive events,
which can be viewed later with the \code{\link{showReactLog}} function.
which can be viewed later with the \code{\link{reactlogShow}} function.
This incurs a substantial performance penalty and should not be used in
production.}
\item{shiny.usecairo}{This is used to disable graphical rendering by the

View File

@@ -3,7 +3,6 @@ Version: 1.0
RestoreWorkspace: No
SaveWorkspace: No
AlwaysSaveHistory: Default
QuitChildProcessesOnExit: Default
EnableCodeIndexing: Yes
UseSpacesForTab: Yes
@@ -18,6 +17,6 @@ StripTrailingWhitespace: Yes
BuildType: Package
PackageUseDevtools: Yes
PackageInstallArgs: --with-keep.source
PackageInstallArgs: --with-keep.source --no-byte-compile
PackageCheckArgs: --as-cran --no-manual --run-donttest
PackageRoxygenize: rd,collate,namespace

View File

@@ -413,17 +413,17 @@ function initShiny() {
// Need to register callbacks for each Bootstrap 3 class.
var bs3classes = ['modal', 'dropdown', 'tab', 'tooltip', 'popover', 'collapse'];
$.each(bs3classes, function(idx, classname) {
$('body').on('shown.bs.' + classname + '.sendImageSize', '*',
$(document.body).on('shown.bs.' + classname + '.sendImageSize', '*',
filterEventsByNamespace('bs', sendImageSize));
$('body').on('shown.bs.' + classname + '.sendOutputHiddenState ' +
$(document.body).on('shown.bs.' + classname + '.sendOutputHiddenState ' +
'hidden.bs.' + classname + '.sendOutputHiddenState',
'*', filterEventsByNamespace('bs', sendOutputHiddenState));
});
// This is needed for Bootstrap 2 compatibility and for non-Bootstrap
// related shown/hidden events (like conditionalPanel)
$('body').on('shown.sendImageSize', '*', sendImageSize);
$('body').on('shown.sendOutputHiddenState hidden.sendOutputHiddenState', '*',
$(document.body).on('shown.sendImageSize', '*', sendImageSize);
$(document.body).on('shown.sendOutputHiddenState hidden.sendOutputHiddenState', '*',
sendOutputHiddenState);
// Send initial pixel ratio, and update it if it changes

View File

@@ -27,6 +27,8 @@ $.extend(checkboxInputBinding, {
if (data.hasOwnProperty('value'))
el.checked = data.value;
// checkboxInput()'s label works different from other
// input labels...the label container should always exist
if (data.hasOwnProperty('label'))
$(el).parent().find('span').text(data.label);

View File

@@ -39,7 +39,7 @@ $.extend(checkboxGroupInputBinding, {
label: this._getLabel($objs[i]) };
}
return { label: $(el).find('label[for="' + $escape(el.id) + '"]').text(),
return { label: this._getLabelNode(el).text(),
value: this.getValue(el),
options: options
};
@@ -59,8 +59,7 @@ $.extend(checkboxGroupInputBinding, {
if (data.hasOwnProperty('value'))
this.setValue(el, data.value);
if (data.hasOwnProperty('label'))
$el.find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
$(el).trigger('change');
},
@@ -72,6 +71,10 @@ $.extend(checkboxGroupInputBinding, {
unsubscribe: function(el) {
$(el).off('.checkboxGroupInputBinding');
},
// Get the DOM element that contains the top-level label
_getLabelNode: function(el) {
return $(el).find('label[for="' + $escape(el.id) + '"]');
},
// Given an input DOM object, get the associated label. Handles labels
// that wrap the input as well as labels associated with 'for' attribute.
_getLabel: function(obj) {

View File

@@ -46,7 +46,7 @@ $.extend(dateInputBinding, {
else if (startview === 0) startview = 'month';
return {
label: $el.find('label[for="' + $escape(el.id) + '"]').text(),
label: this._getLabelNode(el).text(),
value: this.getValue(el),
valueString: $input.val(),
min: min,
@@ -60,8 +60,7 @@ $.extend(dateInputBinding, {
receiveMessage: function(el, data) {
var $input = $(el).find('input');
if (data.hasOwnProperty('label'))
$(el).find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
if (data.hasOwnProperty('min'))
this._setMin($input[0], data.min);
@@ -119,6 +118,9 @@ $.extend(dateInputBinding, {
this._setMax($input[0], $input.data('max-date'));
}
},
_getLabelNode: function(el) {
return $(el).find('label[for="' + $escape(el.id) + '"]');
},
// Given a format object from a date picker, return a string
_formatToString: function(format) {
// Format object has structure like:

View File

@@ -63,7 +63,7 @@ $.extend(dateRangeInputBinding, dateInputBinding, {
else if (startview === 0) startview = 'month';
return {
label: $el.find('label[for="' + $escape(el.id) + '"]').text(),
label: this._getLabelNode(el).text(),
value: this.getValue(el),
valueString: [ $startinput.val(), $endinput.val() ],
min: min,
@@ -80,8 +80,7 @@ $.extend(dateRangeInputBinding, dateInputBinding, {
var $startinput = $inputs.eq(0);
var $endinput = $inputs.eq(1);
if (data.hasOwnProperty('label'))
$el.find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
if (data.hasOwnProperty('min')) {
this._setMin($startinput[0], data.min);
@@ -140,6 +139,9 @@ $.extend(dateRangeInputBinding, dateInputBinding, {
},
unsubscribe: function(el) {
$(el).off('.dateRangeInputBinding');
}
},
_getLabelNode: function(el) {
return $(el).find('label[for="' + $escape(el.id) + '"]');
},
});
inputBindings.register(dateRangeInputBinding, 'shiny.dateRangeInput');

View File

@@ -13,7 +13,7 @@ var IE8FileUploader = function(shinyapp, id, fileEl) {
this.iframe.id = iframeId;
this.iframe.name = iframeId;
this.iframe.setAttribute('style', 'position: fixed; top: 0; left: 0; width: 0; height: 0; border: none');
$('body').append(this.iframe);
$(document.body).append(this.iframe);
var iframeDestroy = function() {
// Forces Shiny to flushReact, flush outputs, etc. Without this we get
// invalidated reactives, but observers don't actually execute.

View File

@@ -24,17 +24,19 @@ $.extend(numberInputBinding, textInputBinding, {
if (data.hasOwnProperty('max')) el.max = data.max;
if (data.hasOwnProperty('step')) el.step = data.step;
if (data.hasOwnProperty('label'))
$(el).parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
$(el).trigger('change');
},
getState: function(el) {
return { label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
return { label: this._getLabelNode(el).text(),
value: this.getValue(el),
min: Number(el.min),
max: Number(el.max),
step: Number(el.step) };
},
_getLabelNode: function(el) {
return $(el).parent().find('label[for="' + $escape(el.id) + '"]');
}
});
inputBindings.register(numberInputBinding, 'shiny.numberInput');

View File

@@ -21,7 +21,7 @@ $.extend(radioInputBinding, {
}
return {
label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
label: this._getLabelNode(el).text(),
value: this.getValue(el),
options: options
};
@@ -41,8 +41,7 @@ $.extend(radioInputBinding, {
if (data.hasOwnProperty('value'))
this.setValue(el, data.value);
if (data.hasOwnProperty('label'))
$(el).parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
$(el).trigger('change');
},
@@ -54,6 +53,10 @@ $.extend(radioInputBinding, {
unsubscribe: function(el) {
$(el).off('.radioInputBinding');
},
// Get the DOM element that contains the top-level label
_getLabelNode: function(el) {
return $(el).parent().find('label[for="' + $escape(el.id) + '"]');
},
// Given an input DOM object, get the associated label. Handles labels
// that wrap the input as well as labels associated with 'for' attribute.
_getLabel: function(obj) {

View File

@@ -22,10 +22,14 @@ $.extend(selectInputBinding, {
return $(el).val();
},
setValue: function(el, value) {
var selectize = this._selectize(el);
if (typeof(selectize) !== 'undefined') {
selectize.setValue(value);
} else $(el).val(value);
if (!this._is_selectize(el)) {
$(el).val(value);
} else {
let selectize = this._selectize(el);
if (selectize) {
selectize.setValue(value);
}
}
},
getState: function(el) {
// Store options in an array of objects, each with with value and label
@@ -36,7 +40,7 @@ $.extend(selectInputBinding, {
}
return {
label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
label: this._getLabelNode(el),
value: this.getValue(el),
options: options
};
@@ -119,8 +123,7 @@ $.extend(selectInputBinding, {
this.setValue(el, data.value);
}
if (data.hasOwnProperty('label'))
$(el).parent().parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
$(el).trigger('change');
},
@@ -141,6 +144,18 @@ $.extend(selectInputBinding, {
initialize: function(el) {
this._selectize(el);
},
_getLabelNode: function(el) {
let escaped_id = $escape(el.id);
if (this._is_selectize(el)) {
escaped_id += "-selectized";
}
return $(el).parent().parent().find('label[for="' + escaped_id + '"]');
},
// Return true if it's a selectize input, false if it's a regular select input.
_is_selectize: function(el) {
var config = $(el).parent().find('script[data-for="' + $escape(el.id) + '"]');
return (config.length > 0);
},
_selectize: function(el, update) {
if (!$.fn.selectize) return undefined;
var $el = $(el);

View File

@@ -130,8 +130,7 @@ $.extend(sliderInputBinding, textInputBinding, {
}
}
if (data.hasOwnProperty('label'))
$el.parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
var domElements = ['data-type', 'time-format', 'timezone'];
for (var i = 0; i < domElements.length; i++) {
@@ -174,7 +173,9 @@ $.extend(sliderInputBinding, textInputBinding, {
$el.ionRangeSlider(opts);
},
_getLabelNode: function(el) {
return $(el).parent().find('label[for="' + $escape(el.id) + '"]');
},
// Number of values; 1 for single slider, 2 for range slider
_numValues: function(el) {
if ($(el).data('ionRangeSlider').options.type === 'double')

View File

@@ -27,8 +27,7 @@ $.extend(textInputBinding, {
if (data.hasOwnProperty('value'))
this.setValue(el, data.value);
if (data.hasOwnProperty('label'))
$(el).parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
updateLabel(data.label, this._getLabelNode(el));
if (data.hasOwnProperty('placeholder'))
el.placeholder = data.placeholder;
@@ -37,7 +36,7 @@ $.extend(textInputBinding, {
},
getState: function(el) {
return {
label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
label: this._getLabelNode(el).text(),
value: el.value,
placeholder: el.placeholder
};
@@ -47,6 +46,9 @@ $.extend(textInputBinding, {
policy: 'debounce',
delay: 250
};
},
_getLabelNode: function(el) {
return $(el).parent().find('label[for="' + $escape(el.id) + '"]');
}
});
inputBindings.register(textInputBinding, 'shiny.textInput');

View File

@@ -189,8 +189,8 @@ var InputBatchSender = function(shinyapp) {
this.lastChanceCallback = [];
};
(function() {
this.setInput = function(name, value, opts) {
this.pendingData[name] = value;
this.setInput = function(nameType, value, opts) {
this.pendingData[nameType] = value;
if (!this.reentrant) {
if (opts.priority === "event") {
@@ -227,8 +227,8 @@ var InputNoResendDecorator = function(target, initialValues) {
this.lastSentValues = this.reset(initialValues);
};
(function() {
this.setInput = function(name, value, opts) {
const { name: inputName, inputType: inputType } = splitInputNameType(name);
this.setInput = function(nameType, value, opts) {
const { name: inputName, inputType: inputType } = splitInputNameType(nameType);
const jsonValue = JSON.stringify(value);
if (opts.priority !== "event" &&
@@ -267,10 +267,10 @@ var InputEventDecorator = function(target) {
this.target = target;
};
(function() {
this.setInput = function(name, value, opts) {
this.setInput = function(nameType, value, opts) {
var evt = jQuery.Event("shiny:inputchanged");
const input = splitInputNameType(name);
const input = splitInputNameType(nameType);
evt.name = input.name;
evt.inputType = input.inputType;
evt.value = value;
@@ -297,31 +297,41 @@ var InputRateDecorator = function(target) {
this.inputRatePolicies = {};
};
(function() {
this.setInput = function(name, value, opts) {
this.$ensureInit(name);
// Note that the first argument of setInput() and setRatePolicy()
// are passed both the input name (i.e., inputId) and type.
// https://github.com/rstudio/shiny/blob/67d3a/srcjs/init_shiny.js#L111-L126
// However, $ensureInit() and $doSetInput() are meant to be passed just
// the input name (i.e., inputId), which is why we distinguish between
// nameType and name.
this.setInput = function(nameType, value, opts) {
const {name: inputName} = splitInputNameType(nameType);
this.$ensureInit(inputName);
if (opts.priority !== "deferred")
this.inputRatePolicies[name].immediateCall(name, value, opts);
this.inputRatePolicies[inputName].immediateCall(nameType, value, opts);
else
this.inputRatePolicies[name].normalCall(name, value, opts);
this.inputRatePolicies[inputName].normalCall(nameType, value, opts);
};
this.setRatePolicy = function(name, mode, millis) {
this.setRatePolicy = function(nameType, mode, millis) {
const {name: inputName} = splitInputNameType(nameType);
if (mode === 'direct') {
this.inputRatePolicies[name] = new Invoker(this, this.$doSetInput);
this.inputRatePolicies[inputName] = new Invoker(this, this.$doSetInput);
}
else if (mode === 'debounce') {
this.inputRatePolicies[name] = new Debouncer(this, this.$doSetInput, millis);
this.inputRatePolicies[inputName] = new Debouncer(this, this.$doSetInput, millis);
}
else if (mode === 'throttle') {
this.inputRatePolicies[name] = new Throttler(this, this.$doSetInput, millis);
this.inputRatePolicies[inputName] = new Throttler(this, this.$doSetInput, millis);
}
};
this.$ensureInit = function(name) {
if (!(name in this.inputRatePolicies))
this.setRatePolicy(name, 'direct');
};
this.$doSetInput = function(name, value, opts) {
this.target.setInput(name, value, opts);
this.$doSetInput = function(nameType, value, opts) {
this.target.setInput(nameType, value, opts);
};
}).call(InputRateDecorator.prototype);
@@ -331,9 +341,9 @@ var InputDeferDecorator = function(target) {
this.pendingInput = {};
};
(function() {
this.setInput = function(name, value, opts) {
if (/^\./.test(name))
this.target.setInput(name, value, opts);
this.setInput = function(nameType, value, opts) {
if (/^\./.test(nameType))
this.target.setInput(nameType, value, opts);
else
this.pendingInput[name] = { value, opts };
};
@@ -352,13 +362,13 @@ const InputValidateDecorator = function(target) {
this.target = target;
};
(function() {
this.setInput = function(name, value, opts) {
if (!name)
this.setInput = function(nameType, value, opts) {
if (!nameType)
throw "Can't set input with empty name.";
opts = addDefaultInputOpts(opts);
this.target.setInput(name, value, opts);
this.target.setInput(nameType, value, opts);
};
}).call(InputValidateDecorator.prototype);
@@ -387,8 +397,8 @@ function addDefaultInputOpts(opts) {
}
function splitInputNameType(name) {
const name2 = name.split(':');
function splitInputNameType(nameType) {
const name2 = nameType.split(':');
return {
name: name2[0],
inputType: name2.length > 1 ? name2[1] : ''

View File

@@ -15,7 +15,7 @@ exports.modal = {
let $modal = $('#shiny-modal-wrapper');
if ($modal.length === 0) {
$modal = $('<div id="shiny-modal-wrapper"></div>');
$('body').append($modal);
$(document.body).append($modal);
// If the wrapper's content is a Bootstrap modal, then when the inner
// modal is hidden, remove the entire thing, including wrapper.

View File

@@ -97,7 +97,7 @@ exports.notifications = (function() {
if ($panel.length > 0)
return $panel;
$('body').append('<div id="shiny-notification-panel">');
$(document.body).append('<div id="shiny-notification-panel">');
return $panel;
}

View File

@@ -7,3 +7,24 @@ $(document).on('keydown', function(e) {
e.preventDefault();
});
$(document).on('keydown', function(e) {
if (e.which !== 115 || (!e.ctrlKey && !e.metaKey) || (e.shiftKey || e.altKey))
return;
var url = 'reactlog/mark?w=' + window.escape(exports.shinyapp.config.workerId) +
"&s=" + window.escape(exports.shinyapp.config.sessionId);
// send notification
$.get(url, function(result) {
if (result !== "marked") return;
var html = '<span id="shiny-reactlog-mark-text">Marked time point in reactlog</span>';
exports.notifications.show({
html: html,
closeButton: true,
});
});
e.preventDefault();
});

View File

@@ -1078,7 +1078,7 @@ var ShinyApp = function() {
var $container = $('.shiny-progress-container');
if ($container.length === 0) {
$container = $('<div class="shiny-progress-container"></div>');
$('body').append($container);
$(document.body).append($container);
}
// Add div for just this progress ID

View File

@@ -326,3 +326,23 @@ exports.compareVersion = function(a, op, b) {
else if (op === "<") return (diff < 0);
else throw `Unknown operator: ${op}`;
};
function updateLabel(labelTxt, labelNode) {
// Only update if label was specified in the update method
if (typeof labelTxt === "undefined") return;
if (labelNode.length !== 1) {
throw new Error("labelNode must be of length 1");
}
// Should the label be empty?
var emptyLabel = $.isArray(labelTxt) && labelTxt.length === 0;
if (emptyLabel) {
labelNode.addClass("shiny-label-null");
} else {
labelNode.text(labelTxt);
labelNode.removeClass("shiny-label-null");
}
}

View File

@@ -51,3 +51,19 @@ test_that("Local options", {
# Finish tests; reset shinyOptions
shinyOptions(a = NULL)
})
test_that("Shared secret", {
op <- options(shiny.sharedSecret = "This is a secret string")
on.exit(options(op))
checkSharedSecret <- loadSharedSecret()
expect_true(checkSharedSecret("This is a secret string"))
expect_true(checkSharedSecret(charToRaw("This is a secret string")))
expect_false(checkSharedSecret("this is a secret string"))
expect_false(checkSharedSecret("This is a secret string "))
expect_false(checkSharedSecret(""))
expect_false(checkSharedSecret(NULL))
expect_error(checkSharedSecret(1:10))
})

View File

@@ -521,8 +521,8 @@ test_that("names() and reactiveValuesToList()", {
# Assigning names fails
expect_error(isolate(names(v) <- c('x', 'y')))
expect_equal(isolate(reactiveValuesToList(values)), list(A=1))
expect_equal(isolate(reactiveValuesToList(values, all.names=TRUE)), list(A=1, .B=2))
expect_mapequal(isolate(reactiveValuesToList(values)), list(A=1))
expect_mapequal(isolate(reactiveValuesToList(values, all.names=TRUE)), list(A=1, .B=2))
flushReact()
@@ -1137,10 +1137,10 @@ test_that("reactive domain works across async handlers", {
~{hasReactiveDomain <<- identical(getDefaultReactiveDomain(), obj)}
)
})
while (is.null(hasReactiveDomain) && !later::loop_empty()) {
later::run_now()
}
testthat::expect_true(hasReactiveDomain)
})

View File

@@ -0,0 +1,131 @@
context("reactlog")
keyValList <- function(key, value) {
ret <- list()
ret[[key]] <- value
ret
}
withOption <- function(key, value, oldVal = NULL, expr) {
oldVal <- getOption(key, oldVal)
do.call("options", keyValList(key, value))
on.exit({
do.call("options", keyValList(key, oldVal))
})
force(expr)
}
withLogging <- function(expr) {
rLog$reset()
# reset ctx counter
reactiveEnvr <- .getReactiveEnvironment()
reactiveEnvr$.nextId <- 0L
withOption("shiny.reactlog", TRUE, FALSE, {
withOption("shiny.reactlog.console", TRUE, FALSE, {
withOption("shiny.suppressMissingContextError", TRUE, FALSE, {
force(expr)
})
})
})
}
expect_logs <- function(expr, ...) {
expected_messages <- unlist(list(...))
captured_messages <- capture_messages(expr)
captured_messages <- sub("\n$", "", captured_messages)
if (length(captured_messages) != length(expected_messages)) {
cat("\nCaptured: \n"); print(captured_messages)
cat("Expected: \n"); print(expected_messages)
}
expect_equal(
captured_messages,
expected_messages
)
}
test_that("rLog resets when options are FALSE", {
withOption("shiny.reactlog", FALSE, FALSE, {
withOption("shiny.reactlog.console", FALSE, FALSE, {
rLog$reset()
# check for dummy and no reactid information
expect_true(!is.null(rLog$noReactId))
expect_true(!is.null(rLog$dummyReactId))
expect_equal(rLog$msg$getReact(rLog$noReactId, force = TRUE)$reactId, rLog$noReactId)
expect_equal(rLog$msg$getReact(rLog$dummyReactId, force = TRUE)$reactId, rLog$dummyReactId)
expect_equal(length(rLog$msg$reactCache), 2)
})
})
})
test_that("message logger appears", {
withLogging({
expect_logs(
{
val <- reactiveVal(1, label = "val")
},
"- define: r1:'val' - reactiveVal ' num 1'"
)
expect_silent(
{
values <- reactiveValues(a = 2, b = 3)
local({
values_obj <- .subset2(values, 'impl')
values_obj$.label <- "values"
})
}
)
expect_logs(
{
react <- reactive(val() + values$a)
},
"- define: r3:'reactive(val() + values$a)' - observable ' NULL'"
)
expect_logs(
{
react()
},
"- createContext: ctxDummy - isolate",
"- dependsOn: rDummyReactId:'DummyReactId' on r3:'reactive(val() + values$a)' in ctxDummy",
"- createContext: ctx1 - observable",
"- enter: r3:'reactive(val() + values$a)' in ctx1 - observable",
"= - dependsOn: r3:'reactive(val() + values$a)' on r1:'val' in ctx1",
"= - define: r2$a:'values$a' - reactiveValuesKey ' num 2'",
"= - dependsOn: r3:'reactive(val() + values$a)' on r2$a:'values$a' in ctx1",
"- exit: r3:'reactive(val() + values$a)' in ctx1 - observable"
)
expect_logs(
{
val(4)
},
"- valueChange: r1:'val' ' num 4'",
"- invalidateStart: r1:'val'",
"= - invalidateStart: r3:'reactive(val() + values$a)' in ctx1 - observable",
"= = - isolateInvalidateStart: rDummyReactId:'DummyReactId' in ctxDummy",
"= = = - dependsOnRemove: rDummyReactId:'DummyReactId' on r3:'reactive(val() + values$a)' in ctxDummy",
"= = - isolateInvalidateEnd: rDummyReactId:'DummyReactId' in ctxDummy",
"= = - dependsOnRemove: r3:'reactive(val() + values$a)' on r1:'val' in ctx1",
"= = - dependsOnRemove: r3:'reactive(val() + values$a)' on r2$a:'values$a' in ctx1",
"= - invalidateEnd: r3:'reactive(val() + values$a)' in ctx1 - observable",
"- invalidateEnd: r1:'val'"
)
expect_logs(
{values$a <- 5},
"- valueChange: r2$a:'values$a' ' num 5'",
"- invalidateStart: r2$a:'values$a'",
"- invalidateEnd: r2$a:'values$a'"
)
})
})

View File

@@ -31,7 +31,7 @@ Periodically, it's good to upgrade the packages to a recent version. There's two
1. Use `yarn upgrade` to upgrade all dependencies to their latest version based on the version range specified in the package.json file (the yarn.lock file will be recreated as well. Yarn packages use [semantic versioning](https://yarnpkg.com/en/docs/dependency-versions), i.e. each version is writen with a maximum of 3 dot-separated numbers such that: `major.minor.patch`. For example in the version `3.1.4`, 3 is the major version number, 1 is the minor version number and 4 is the patch version number. Here are the most used operators (these appear before the version number):
- `~` is for upgrades that keep the minor version the same (assuming that was specified);
- `^` is for upgrades that keep the major version the same (more or less -- more specifically, it allow changes that do not modify the first non-zero digit in the version, either the 3 in 3.1.4 or the 4 in 0.4.2.). This is the default operator added to the package.json when you run `yarn add [package-name]`.
2. Use `yarn upgrade [package]` to upgrade a single named package to the version specified by the latest tag (potentially upgrading the package across major versions).
@@ -41,52 +41,32 @@ For more information about upgrading or installing new packages, see the [yarn w
### Grunt
Grunt is a build tool that runs on node.js (and installed using `yarn`). In Shiny, it is used for concatenating, minifying, and linting Javascript code.
#### Installing Grunt and the Grunt CLI (command line interface)
Grunt is a package listed in package.json, so if you've done the previous step, that's already installed. However, as a developer, you also need to install a sister package (called `grunt-cli`) globally:
```
# Install grunt command line tool globally
sudo yarn global add grunt-cli
```
Here's what has happened (from the [Grunt Getting Started guide](http://gruntjs.com/getting-started)):
> This will put the `grunt` command in your system path, allowing it to be run from any directory.
>
> Note that installing `grunt-cli` does not install the Grunt task runner! The job of the Grunt CLI is simple: run the version of Grunt which has been installed next to a `Gruntfile`. This allows multiple versions of Grunt to be installed on the same machine simultaneously.
And here is how the CLI works (same source):
> Each time `grunt` is run, it looks for a locally installed Grunt using node's `require()` system. Because of this, you can run `grunt` from any subfolder in your project.
>
> If a locally installed Grunt is found, the CLI loads the local installation of the Grunt library, applies the configuration from your `Gruntfile`, and executes any tasks you've requested for it to run. To really understand what is happening, [read the code](https://github.com/gruntjs/grunt-cli/blob/master/bin/grunt).
### Using Grunt
To run all default grunt tasks specified in the Gruntfile (concatenation, minification, and jshint), simply go into the `tools` directory and run:
```
grunt
yarn build
```
Sometimes grunt gets confused about whether the output files are up to date, and won't overwrite them even if the input files have changed. If this happens, run:
```
grunt clean
yarn clean
```
It's also useful to run `grunt` so that it monitors files for changes and run tasks as necessary. This is done with:
```
grunt watch
yarn watch
```
One of the tasks concatenates all the .js files in `/srcjs` together into `/inst/www/shared/shiny.js`. Another task minifies `shiny.js` to generate `shiny.min.js`. The minified file is supplied to the browser, along with a source map file, `shiny.min.js.map`, which allows a user to view the original Javascript source when using the debugging console in the browser.
During development of Shiny's Javascript code, it's best to use `grunt watch` so that the minified file will get updated whenever you make changes the Javascript sources.
During development of Shiny's Javascript code, it's best to use `yarn watch` so that the minified file will get updated whenever you make changes the Javascript sources.
#### Auto build and browser refresh
An alternative to `grunt watch` is to use `entr` to trigger `grunt` when sources change. `entr` can be installed with `brew install entr` on a Mac, or on Linux using your distribution's package manager. Using this technique, it's possible to both automatically rebuild sources and reload Chrome at the same time:
An alternative to `yarn watch` is to use `entr` to trigger `grunt` when sources change. `entr` can be installed with `brew install entr` on a Mac, or on Linux using your distribution's package manager. Using this technique, it's possible to both automatically rebuild sources and reload Chrome at the same time:
*macOS*:
@@ -112,3 +92,27 @@ To update the version of babel-polyfill:
* Run `yarn add --dev babel-polyfill --exact`.
* Edit R/shinyui.R. The `renderPage` function has an `htmlDependency` for
`babel-polyfill`. Update this to the new version number.
# Updating and patching `bootstrap-datepicker`
## Updating
[bootstrap-datepicker](https://github.com/uxsolutions/bootstrap-datepicker) can be updated with the script `updateBootstrapDatepicker.R`.
After updating, our patches to `bootstrap-datepicker` must be applied using the script `applyDatepickerPatches.R`
After updating and applying patches, `yarn grunt` should be run per the instructions above in order to generate a minified JavaScript file.
## Making a new patch
To create a new patch:
1. Make any necessary changes to files in `inst/www/shared/datepicker`
1. **Do not commit your changes.**
1. Instead, create a patch with a command like `git diff > tools/datepicker-patches/012-a-description.patch`. Patches are applied in alphabetic order (per `list.files`), so you should name your patch based on the last one in `tools/datepicker-patches` so that it's applied last.
1. Add the new `.patch` file to the repo with a descriptive commit message
1. Revert `bootstrap-datepicker` to its unpatched state by running `updateBootstrapDatepicker.R`
1. Apply all patches, including the one you just made, by running `applyDatepickerPatches.R`
1. Run `yarn grunt`
1. Test your changes
1. `git add` the new `.patch` and any resulting changes

18
tools/applyDatepickerPatches.R Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env Rscript
# Applies patches stored in tools/datepicker-patches
# Should be run after running tools/updateBootstrapDatepicker.R
library(rprojroot)
patch_dir <- rprojroot::find_package_root_file("tools/datepicker-patches")
for (patch in list.files(patch_dir, full.names = TRUE)) {
tryCatch({
message(sprintf("Applying %s", basename(patch)))
system(sprintf("git apply '%s'", patch))
},
error = function(e) {
quit(save = "no", status = 1)
}
)
}

View File

@@ -0,0 +1,23 @@
diff --git a/inst/www/shared/datepicker/js/bootstrap-datepicker.js b/inst/www/shared/datepicker/js/bootstrap-datepicker.js
index 76a99fc2..97f5c086 100644
--- a/inst/www/shared/datepicker/js/bootstrap-datepicker.js
+++ b/inst/www/shared/datepicker/js/bootstrap-datepicker.js
@@ -529,7 +529,17 @@
},
_utc_to_local: function(utc){
- return utc && new Date(utc.getTime() + (utc.getTimezoneOffset()*60000));
+
+ if (!utc) return utc;
+
+ var local = new Date(utc.getTime() + (utc.getTimezoneOffset() * 60000));
+
+ if (local.getTimezoneOffset() != utc.getTimezoneOffset())
+ {
+ local = new Date(utc.getTime() + (local.getTimezoneOffset() * 60000));
+ }
+
+ return utc && local;
},
_local_to_utc: function(local){
return local && new Date(local.getTime() - (local.getTimezoneOffset()*60000));

View File

@@ -0,0 +1,31 @@
diff --git a/inst/www/shared/datepicker/js/bootstrap-datepicker.js b/inst/www/shared/datepicker/js/bootstrap-datepicker.js
index 97f5c086..2a0d8ae6 100644
--- a/inst/www/shared/datepicker/js/bootstrap-datepicker.js
+++ b/inst/www/shared/datepicker/js/bootstrap-datepicker.js
@@ -671,7 +671,7 @@
visualPadding = 10,
container = $(this.o.container),
windowWidth = container.width(),
- scrollTop = this.o.container === 'body' ? $(document).scrollTop() : container.scrollTop(),
+ scrollTop = this.o.container === 'body:first' ? $(document).scrollTop() : container.scrollTop(),
appendOffset = container.offset();
var parentsZindex = [];
@@ -686,7 +686,7 @@
var left = offset.left - appendOffset.left,
top = offset.top - appendOffset.top;
- if (this.o.container !== 'body') {
+ if (this.o.container !== 'body:first') {
top += scrollTop;
}
@@ -1766,7 +1766,7 @@
enableOnReadonly: true,
showOnFocus: true,
zIndexOffset: 10,
- container: 'body',
+ container: 'body:first',
immediateUpdates: false,
title: '',
templates: {

View File

@@ -1,4 +1,10 @@
{
"private": true,
"scripts": {
"build": "grunt",
"clean": "grunt clean",
"watch": "grunt default watch"
},
"devDependencies": {
"babel-eslint": "^6.0.0",
"babel-preset-es2015": "^6.6.0",

View File

@@ -1,47 +1,41 @@
#!/usr/bin/env Rscript
# Retrieves a particular version of bootstrap-datepicker:
# https://github.com/uxsolutions/bootstrap-datepicker
# After retrieving, you can apply patches stored in
# tools/datepicker-patches with applyDatepickerPatches.R
# This script copies resources from Bootstrap Datepicker to shiny's inst
# directory. The bootstrap-datepicker/ project directory should be on the same
# level as the shiny/ project directory.
library(rprojroot)
# It is necessary to run Grunt after running this script: This copies the
# un-minified JS file over, and running Grunt minifies it and inlines the locale
# files into the minified JS.
version <- "1.6.4"
dest_dir <- rprojroot::find_package_root_file("inst/www/shared/datepicker")
tag <- paste0("v", version)
dest_file <- file.path(tempdir(), paste0("bootstrap-datepicker-", version, ".zip"))
url <- sprintf("https://github.com/uxsolutions/bootstrap-datepicker/releases/download/%s/bootstrap-datepicker-%s-dist.zip", tag, version)
# This script can be sourced from RStudio, or run with Rscript.
download.file(url, dest_file)
unzipped <- tempdir()
unzip(dest_file, exdir = unzipped)
# Returns the file currently being sourced or run with Rscript
thisFile <- function() {
cmdArgs <- commandArgs(trailingOnly = FALSE)
needle <- "--file="
match <- grep(needle, cmdArgs)
if (length(match) > 0) {
# Rscript
return(normalizePath(sub(needle, "", cmdArgs[match])))
} else {
# 'source'd via R console
return(normalizePath(sys.frames()[[1]]$ofile))
}
}
srcdir <- normalizePath(file.path(dirname(thisFile()), "../../bootstrap-datepicker/dist"))
destdir <- normalizePath(file.path(dirname(thisFile()), "../inst/www/shared/datepicker"))
unlink(dest_dir, recursive = TRUE)
dir.create(file.path(dest_dir, "js"), recursive = TRUE)
file.copy(
file.path(srcdir, "js", "bootstrap-datepicker.js"),
file.path(destdir, "js"),
file.path(unzipped, "js", "bootstrap-datepicker.js"),
file.path(dest_dir, "js"),
overwrite = TRUE
)
dir.create(file.path(dest_dir, "js", "locales"), recursive = TRUE)
file.copy(
dir(file.path(srcdir, "locales"), "\\.js$", full.names = TRUE),
file.path(destdir, "js", "locales"),
dir(file.path(unzipped, "locales"), "\\.js$", full.names = TRUE),
file.path(dest_dir, "js", "locales"),
overwrite = TRUE
)
dir.create(file.path(dest_dir, "css"), recursive = TRUE)
file.copy(
dir(file.path(srcdir, "css"), "^bootstrap-datepicker3(\\.min)?\\.css$",
dir(file.path(unzipped, "css"), "^bootstrap-datepicker3(\\.min)?\\.css$",
full.names = TRUE),
file.path(destdir, "css"),
file.path(dest_dir, "css"),
overwrite = TRUE
)