Compare commits

..

89 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
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
50 changed files with 814 additions and 466 deletions

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 1.2.0.9001
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,7 +65,7 @@ Depends:
Imports:
utils,
grDevices,
httpuv (>= 1.4.5.9004),
httpuv (>= 1.5.0),
mime (>= 0.3),
jsonlite (>= 0.9.16),
xtable,
@@ -77,22 +77,22 @@ Imports:
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 (>= 0.0.0.9003),
reactlog (>= 1.0.0),
magrittr
Remotes:
wch/fastmap
URL: http://shiny.rstudio.com
BugReports: https://github.com/rstudio/shiny/issues
Remotes:
rstudio/httpuv,
rstudio/reactlog
Collate:
'app.R'
'bookmark-state-local.R'

View File

@@ -301,5 +301,6 @@ import(httpuv)
import(methods)
import(mime)
import(xtable)
importFrom(fastmap,fastmap)
importFrom(grDevices,dev.cur)
importFrom(grDevices,dev.set)

49
NEWS.md
View File

@@ -1,5 +1,45 @@
shiny 1.2.0.9001
================
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
@@ -21,12 +61,15 @@ shiny 1.2.0.9001
* 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
===========
@@ -146,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.

16
R/app.R
View File

@@ -228,7 +228,13 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
structure(
list(
staticPaths = staticPaths,
httpHandler = joinHandlers(c(uiHandler, fallbackWWWDir)),
# 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,
onStop = onStop,
@@ -355,7 +361,13 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
# uiHandler, then falbackWWWDir (which is served up by the R
# staticHandler function).
staticPaths = staticPaths,
httpHandler = joinHandlers(c(dynHttpHandler, fallbackWWWDir)),
# 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,
onStop = onStop,

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

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

View File

@@ -9,30 +9,30 @@ is_installed <- function(package, version) {
# @param version The version of the package
check_suggested <- function(package, version, location) {
if (!is_installed(package, version)) {
missing_location <- missing(location)
msg <- paste0(
sQuote(package),
if (is.na(version)) "" else paste0("(>= ", version, ")"),
" must be installed for this functionality.",
if (!missing_location)
paste0(
"\nPlease install the missing package: \n",
" source(\"https://install-github.me/", location, "\")"
)
)
if (is_installed(package, version)) {
return()
}
if (interactive() && missing_location) {
message(msg, "\nWould you like to install it?")
if (utils::menu(c("Yes", "No")) == 1) {
utils::install.packages(package)
} else {
stop(msg, call. = FALSE)
}
} else {
stop(msg, call. = FALSE)
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)
}
@@ -127,7 +127,7 @@ renderReactlog <- function(sessionToken = NULL, time = TRUE) {
)
}
check_reactlog <- function() {
check_suggested("reactlog", reactlog_version(), "rstudio/reactlog")
check_suggested("reactlog", reactlog_version())
}
# read reactlog version from description file
# prevents version mismatch in code and description file
@@ -200,8 +200,13 @@ RLog <- R6Class(
},
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, {
utils::capture.output(utils::str(value))
# 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)

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

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

@@ -21,28 +21,28 @@ Dependents <- R6Class(
# 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)
# 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)
# rLog$dependsOnRemove(ctx$.reactId, .reactId, ctx$id, ctx$.domain)
.dependents$remove(ctx$id)
})
}
},
# at times, the context is run in a ctx$onInvalidate(...) which has no runtime context
invalidate = function(log = TRUE) {
if (isTRUE(log)) {
# if (isTRUE(log)) {
domain <- getDefaultReactiveDomain()
rLog$invalidateStart(.reactId, NULL, "other", domain)
on.exit(
rLog$invalidateEnd(.reactId, NULL, "other", domain),
add = TRUE
)
}
# domain <- getDefaultReactiveDomain()
# rLog$invalidateStart(.reactId, NULL, "other", domain)
# on.exit(
# rLog$invalidateEnd(.reactId, NULL, "other", domain),
# add = TRUE
# )
# }
lapply(
.dependents$values(),
function(ctx) {
@@ -74,7 +74,7 @@ ReactiveVal <- R6Class(
private$value <- value
private$label <- label
private$dependents <- Dependents$new(reactId = private$reactId)
rLog$define(private$reactId, value, private$label, type = "reactiveVal", getDefaultReactiveDomain())
# rLog$define(private$reactId, value, private$label, type = "reactiveVal", getDefaultReactiveDomain())
},
get = function() {
private$dependents$register()
@@ -88,7 +88,7 @@ ReactiveVal <- R6Class(
if (identical(private$value, value)) {
return(invisible(FALSE))
}
rLog$valueChange(private$reactId, value, getDefaultReactiveDomain())
# rLog$valueChange(private$reactId, value, getDefaultReactiveDomain())
private$value <- value
private$dependents$invalidate()
invisible(TRUE)
@@ -97,14 +97,14 @@ ReactiveVal <- R6Class(
if (is.null(session)) {
stop("Can't freeze a reactiveVal without a reactive domain")
}
rLog$freezeReactiveVal(private$reactId, session)
# rLog$freezeReactiveVal(private$reactId, session)
session$onFlushed(function() {
self$thaw(session)
})
private$frozen <- TRUE
},
thaw = function(session = getDefaultReactiveDomain()) {
rLog$thawReactiveVal(private$reactId, session)
# rLog$thawReactiveVal(private$reactId, session)
private$frozen <- FALSE
},
isFrozen = function() {
@@ -292,9 +292,9 @@ ReactiveValues <- R6Class(
# 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
@@ -312,9 +312,9 @@ ReactiveValues <- R6Class(
) {
.reactId <<- nextGlobalReactId()
.label <<- label
.values <<- new.env(parent=emptyenv())
.metadata <<- new.env(parent=emptyenv())
.dependents <<- new.env(parent=emptyenv())
.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))
@@ -324,27 +324,24 @@ ReactiveValues <- R6Class(
get = function(key) {
# get value right away to use for logging
if (!exists(key, envir=.values, inherits=FALSE))
keyValue <- NULL
else
keyValue <- .values[[key]]
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 <- getCurrentContext()
dep.key <- paste(key, ':', ctx$id, sep='')
if (!exists(dep.key, envir=.dependents, inherits=FALSE)) {
reactKeyId <- rLog$keyIdStr(.reactId, key)
if (!.dependents$containsKey(dep.key)) {
# reactKeyId <- rLog$keyIdStr(.reactId, key)
if (!isTRUE(.hasRetrieved$keys[[key]])) {
rLog$defineKey(.reactId, keyValue, key, .label, ctx$.domain)
# rLog$defineKey(.reactId, keyValue, key, .label, ctx$.domain)
.hasRetrieved$keys[[key]] <<- TRUE
}
rLog$dependsOnKey(ctx$.reactId, .reactId, key, ctx$id, ctx$.domain)
.dependents[[dep.key]] <- ctx
# rLog$dependsOnKey(ctx$.reactId, .reactId, key, ctx$id, ctx$.domain)
.dependents$set(dep.key, ctx)
ctx$onInvalidate(function() {
rLog$dependsOnKeyRemove(ctx$.reactId, .reactId, key, ctx$id, ctx$.domain)
rm(list=dep.key, envir=.dependents, inherits=FALSE)
# rLog$dependsOnKeyRemove(ctx$.reactId, .reactId, key, ctx$id, ctx$.domain)
.dependents$remove(dep.key)
})
}
@@ -384,57 +381,53 @@ ReactiveValues <- R6Class(
domain <- getDefaultReactiveDomain()
hidden <- substr(key, 1, 1) == "."
key_exists <- exists(key, envir=.values, inherits=FALSE)
key_exists <- .values$containsKey(key)
if (key_exists) {
if (.dedupe && identical(.values[[key]], value)) {
if (.dedupe && identical(.values$get(key), value)) {
return(invisible())
}
}
# set the value for better logging
.values[[key]] <- value
.values$set(key, value)
if (key_exists) {
# key has been depended upon (can not happen if the key is being set)
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
)
}
# 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
# )
# }
} else {
# only invalidate if there are deps
if (isTRUE(.hasRetrieved$names)) {
rLog$valueChangeNames(.reactId, ls(.values, all.names = TRUE), domain)
.namesDeps$invalidate()
}
# only invalidate if there are deps
if (!key_exists && isTRUE(.hasRetrieved$names)) {
# rLog$valueChangeNames(.reactId, .values$keys(), domain)
.namesDeps$invalidate()
}
if (hidden) {
if (isTRUE(.hasRetrieved$asListAll)) {
rLog$valueChangeAsListAll(.reactId, as.list(.values, all.names = TRUE), domain)
# 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, as.list(.values, all.names = FALSE), domain)
# rLog$valueChangeAsList(.reactId, react_vals, domain)
.valuesDeps$invalidate()
}
}
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
@@ -451,10 +444,10 @@ ReactiveValues <- R6Class(
},
names = function() {
nameValues <- ls(.values, all.names=TRUE)
nameValues <- .values$keys()
if (!isTRUE(.hasRetrieved$names)) {
domain <- getDefaultReactiveDomain()
rLog$defineNames(.reactId, nameValues, .label, domain)
# rLog$defineNames(.reactId, nameValues, .label, domain)
.hasRetrieved$names <<- TRUE
}
.namesDeps$register()
@@ -465,7 +458,7 @@ ReactiveValues <- R6Class(
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.
@@ -473,24 +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)
# rLog$freezeReactiveKey(.reactId, key, domain)
setMeta(key, "frozen", TRUE)
},
thaw = function(key) {
domain <- getDefaultReactiveDomain()
rLog$thawReactiveKey(.reactId, key, domain)
# rLog$thawReactiveKey(.reactId, key, domain)
setMeta(key, "frozen", NULL)
},
@@ -499,11 +494,14 @@ ReactiveValues <- R6Class(
},
toList = function(all.names=FALSE) {
listValue <- as.list(.values, all.names=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)
# rLog$defineAsListAll(.reactId, listValue, .label, domain)
.hasRetrieved$asListAll <<- TRUE
}
.allValuesDeps$register()
@@ -512,7 +510,7 @@ ReactiveValues <- R6Class(
if (!isTRUE(.hasRetrieved$asList)) {
domain <- getDefaultReactiveDomain()
# making sure the value being recorded is with `all.names = FALSE`
rLog$defineAsList(.reactId, as.list(.values, all.names=FALSE), .label, domain)
# rLog$defineAsList(.reactId, listValue[!grepl("^\\.", base::names(listValue))], .label, domain)
.hasRetrieved$asList <<- TRUE
}
.valuesDeps$register()
@@ -832,7 +830,7 @@ Observable <- R6Class(
.running <<- FALSE
.execCount <<- 0L
.mostRecentCtxId <<- ""
rLog$define(.reactId, .value, .label, type = "observable", .domain)
# rLog$define(.reactId, .value, .label, type = "observable", .domain)
},
getValue = function() {
.dependents$register()
@@ -1082,7 +1080,7 @@ Observer <- R6Class(
setAutoDestroy(autoDestroy)
.reactId <<- nextGlobalReactId()
rLog$defineObserver(.reactId, .label, .domain)
# rLog$defineObserver(.reactId, .label, .domain)
# Defer the first running of this until flushReact is called
.createContext()$invalidate()
@@ -1607,7 +1605,7 @@ invalidateLater <- function(millis, session = getDefaultReactiveDomain()) {
force(session)
ctx <- getCurrentContext()
rLog$invalidateLater(ctx$.reactId, ctx$id, millis, session)
# rLog$invalidateLater(ctx$.reactId, ctx$id, millis, session)
timerHandle <- scheduleTask(millis, function() {
if (is.null(session)) {

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.
@@ -23,6 +25,7 @@ registerClient <- function(client) {
.globals$resourcePaths <- list()
.globals$resources <- list()
.globals$showcaseDefault <- 0
@@ -69,8 +72,62 @@ addResourcePath <- function(prefix, directoryPath) {
getShinyOption("server")$setStaticPath(.list = stats::setNames(normalizedPath, prefix))
}
# .globals$resourcePaths persists across runs of applications.
.globals$resourcePaths[[prefix]] <- normalizedPath
# .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='')
return(resInfo$func(subreq))
}
#' Define Server Functionality
@@ -158,6 +215,8 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
appvars <- new.env()
appvars$server <- NULL
sys.www.root <- system.file('www', package='shiny')
# This value, if non-NULL, must be present on all HTTP and WebSocket
# requests as the Shiny-Shared-Secret header or else access will be
# denied (403 response for HTTP, and instant close for websocket).
@@ -167,6 +226,8 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
http = joinHandlers(c(
sessionHandler,
httpHandlers,
sys.www.root,
resourcePathHandler,
reactLogHandler
)),
ws = function(ws) {

View File

@@ -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))
@@ -1204,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
@@ -1912,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 {
@@ -2215,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

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

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.9001"; // 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');
@@ -5113,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
};
@@ -5195,13 +5245,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.setValue(el, data.value);
}
if (data.hasOwnProperty('label')) {
var escaped_id = $escape(el.id);
if (this._is_selectize(el)) {
escaped_id += "-selectized";
}
$(el).parent().parent().find('label[for="' + escaped_id + '"]').text(data.label);
}
updateLabel(data.label, this._getLabelNode(el));
$(el).trigger('change');
},
@@ -5224,6 +5268,13 @@ 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) + '"]');
@@ -5301,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
};
@@ -5320,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');
},
@@ -5332,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) {
@@ -5397,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
};
@@ -5416,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');
},
@@ -5428,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) {
@@ -5593,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.
@@ -6457,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();

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

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

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

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

@@ -40,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
};
@@ -123,15 +123,7 @@ $.extend(selectInputBinding, {
this.setValue(el, data.value);
}
if (data.hasOwnProperty('label')) {
let escaped_id = $escape(el.id);
if (this._is_selectize(el)) {
escaped_id += "-selectized";
}
$(el).parent().parent()
.find('label[for="' + escaped_id + '"]')
.text(data.label);
}
updateLabel(data.label, this._getLabelNode(el));
$(el).trigger('change');
},
@@ -152,6 +144,13 @@ $.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) + '"]');

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

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

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

@@ -92,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,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
)