mirror of
https://github.com/rstudio/shiny.git
synced 2026-04-07 03:00:20 -04:00
Merge branch 'master' into bookmarkable-state
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
Package: shiny
|
||||
Type: Package
|
||||
Title: Web Application Framework for R
|
||||
Version: 0.13.2.9003
|
||||
Version: 0.13.2.9004
|
||||
Date: 2016-02-17
|
||||
Authors@R: c(
|
||||
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
|
||||
|
||||
@@ -11,6 +11,7 @@ S3method("[",shinyoutput)
|
||||
S3method("[<-",reactivevalues)
|
||||
S3method("[<-",shinyoutput)
|
||||
S3method("[[",reactivevalues)
|
||||
S3method("[[",session_proxy)
|
||||
S3method("[[",shinyoutput)
|
||||
S3method("[[<-",reactivevalues)
|
||||
S3method("[[<-",shinyoutput)
|
||||
|
||||
14
NEWS
14
NEWS
@@ -1,5 +1,11 @@
|
||||
shiny 0.13.2.9001
|
||||
--------------------------------------------------------------------------------
|
||||
* Added support for the `pool` package (use Shiny's timer/scheduler)
|
||||
|
||||
* `Display: Showcase` now displays the .js, .html and .css files in the `www`
|
||||
directory by default. In order to use showcase mode and not display these,
|
||||
include a new line in your Description file: `IncludeWWW: False`.
|
||||
|
||||
* Added insertUI and removeUI functions to be able to add and remove chunks
|
||||
of UI, standalone, and all independent of one another.
|
||||
|
||||
@@ -28,6 +34,8 @@ shiny 0.13.2.9001
|
||||
`showNotification` and `hideNotification`. From JavaScript, there is a new
|
||||
`Shiny.notification` object that controls notifications. (#1141)
|
||||
|
||||
* Progress indicators now use the notification API.
|
||||
|
||||
* Improved `renderTable()` function to make the tables look prettier and also
|
||||
provide the user with a lot more parameters to customize their tables with.
|
||||
|
||||
@@ -71,6 +79,12 @@ shiny 0.13.2.9001
|
||||
* Almost all code examples now have a runnable example with `shinyApp()`, so
|
||||
that users can run the examples and see them in action. (#1137, #1158)
|
||||
|
||||
* Added `session$resetBrush(brushId)` (R) and `Shiny.resetBrush(brushId)` (JS)
|
||||
to programatically clear brushes from `imageOutput`/`plotOutput`.
|
||||
|
||||
* Fixed #1253: Memory could leak when an observer was destroyed without first
|
||||
being invalidated.
|
||||
|
||||
shiny 0.13.2
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -12,22 +12,11 @@
|
||||
#' appropriate \code{render} function or a customized \code{reactive}
|
||||
#' function. To remove any part of your UI, use \code{\link{removeUI}}.
|
||||
#'
|
||||
#' Note that whatever UI object you pass through \code{ui}, it is always
|
||||
#' wrapped in an extra \code{div} (or if \code{inline = TRUE}, a
|
||||
#' \code{span}) before making its way into the DOM. This does not affect
|
||||
#' what you mean to do, and it makes it easier to remove the whole UI
|
||||
#' object using \code{\link{removeUI}} (if you wish to do so, of course).
|
||||
#'
|
||||
#' @param selector A string that is accepted by jQuery's selector (i.e. the
|
||||
#' string \code{s} to be placed in a \code{$(s)} jQuery call). This selector
|
||||
#' will determine the element(s) relative to which you want to insert your
|
||||
#' UI object.
|
||||
#'
|
||||
#' @param multiple In case your selector matches more than one element,
|
||||
#' \code{multiple} determines whether Shiny should insert the UI object
|
||||
#' relative to all matched elements or just relative to the first
|
||||
#' matched element (default).
|
||||
#'
|
||||
#' @param where Where your UI object should go relative to the selector:
|
||||
#' \describe{
|
||||
#' \item{\code{beforeBegin}}{Before the selector element itself}
|
||||
@@ -41,18 +30,22 @@
|
||||
#' \href{https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML}{here}.
|
||||
#'
|
||||
#' @param ui The UI object you want to insert. This can be anything that
|
||||
#' you usually put inside your apps's \code{ui} function.
|
||||
#' you usually put inside your apps's \code{ui} function. If you're inserting
|
||||
#' multiple elements in one call, make sure to wrap them in either a
|
||||
#' \code{tagList()} or a \code{tags$div()} (the latter option has the
|
||||
#' advantage that you can give it an \code{id} to make it easier to
|
||||
#' reference or remove it later on). If you want to insert raw html, use
|
||||
#' \code{ui = HTML()}.
|
||||
#'
|
||||
#' @param multiple In case your selector matches more than one element,
|
||||
#' \code{multiple} determines whether Shiny should insert the UI object
|
||||
#' relative to all matched elements or just relative to the first
|
||||
#' matched element (default).
|
||||
#'
|
||||
#' @param immediate Whether the UI object should be immediately inserted into
|
||||
#' the app when you call \code{insertUI}, or whether Shiny should wait until
|
||||
#' all outputs have been updated and all observers have been run (default).
|
||||
#'
|
||||
#' @param container A function to generate an HTML element to contain the UI
|
||||
#' object.
|
||||
#'
|
||||
#' @param inline Use an inline (\code{span()}) or block container (\code{div()},
|
||||
#' default) for the output.
|
||||
#'
|
||||
#' @param session The shiny session within which to call \code{insertUI}.
|
||||
#'
|
||||
#' @seealso \code{\link{removeUI}}
|
||||
@@ -83,19 +76,16 @@
|
||||
#'
|
||||
#' @export
|
||||
insertUI <- function(selector,
|
||||
multiple = FALSE,
|
||||
where = c("beforeBegin", "afterBegin", "beforeEnd", "afterEnd"),
|
||||
ui,
|
||||
multiple = FALSE,
|
||||
immediate = FALSE,
|
||||
container = if (inline) "span" else "div",
|
||||
inline = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(selector)
|
||||
force(ui)
|
||||
force(session)
|
||||
force(multiple)
|
||||
force(container)
|
||||
if (missing(where)) where <- "beforeEnd"
|
||||
where <- match.arg(where)
|
||||
|
||||
@@ -103,8 +93,7 @@ insertUI <- function(selector,
|
||||
session$sendInsertUI(selector = selector,
|
||||
multiple = multiple,
|
||||
where = where,
|
||||
content = processDeps(ui, session),
|
||||
container = container)
|
||||
content = processDeps(ui, session))
|
||||
}
|
||||
|
||||
if (!immediate) session$onFlushed(callback, once = TRUE)
|
||||
@@ -129,7 +118,9 @@ insertUI <- function(selector,
|
||||
#' string \code{s} to be placed in a \code{$(s)} jQuery call). This selector
|
||||
#' will determine the element(s) to be removed. If you want to remove a
|
||||
#' Shiny input or output, note that many of these are wrapped in \code{div}s,
|
||||
#' so you may need to use a somewhat complex selector (see the Examples below).
|
||||
#' so you may need to use a somewhat complex selector -- see the Examples below.
|
||||
#' (Alternatively, you could also wrap the inputs/outputs that you want to be
|
||||
#' able to remove easily in a \code{div} with an id.)
|
||||
#'
|
||||
#' @param multiple In case your selector matches more than one element,
|
||||
#' \code{multiple} determines whether Shiny should remove all the matched
|
||||
|
||||
18
R/modules.R
18
R/modules.R
@@ -1,4 +1,4 @@
|
||||
# Creates an object whose $ and $<- pass through to the parent
|
||||
# Creates an object whose $ and [[ pass through to the parent
|
||||
# session, unless the name is matched in ..., in which case
|
||||
# that value is returned instead. (See Decorator pattern.)
|
||||
createSessionProxy <- function(parentSession, ...) {
|
||||
@@ -14,18 +14,24 @@ createSessionProxy <- function(parentSession, ...) {
|
||||
|
||||
#' @export
|
||||
`$.session_proxy` <- function(x, name) {
|
||||
if (name %in% names(x[["overrides"]]))
|
||||
x[["overrides"]][[name]]
|
||||
if (name %in% names(.subset2(x, "overrides")))
|
||||
.subset2(x, "overrides")[[name]]
|
||||
else
|
||||
x[["parent"]][[name]]
|
||||
.subset2(x, "parent")[[name]]
|
||||
}
|
||||
|
||||
#' @export
|
||||
`[[.session_proxy` <- `$.session_proxy`
|
||||
|
||||
|
||||
#' @export
|
||||
`$<-.session_proxy` <- function(x, name, value) {
|
||||
x[["parent"]][[name]] <- value
|
||||
x
|
||||
stop("Attempted to assign value on session proxy.")
|
||||
}
|
||||
|
||||
`[[<-.session_proxy` <- `$<-.session_proxy`
|
||||
|
||||
|
||||
#' Invoke a Shiny module
|
||||
#'
|
||||
#' Shiny's module feature lets you break complicated UI and server logic into
|
||||
|
||||
@@ -42,11 +42,11 @@ NULL
|
||||
#
|
||||
## ------------------------------------------------------------------------
|
||||
createMockDomain <- function() {
|
||||
callbacks <- list()
|
||||
callbacks <- Callbacks$new()
|
||||
ended <- FALSE
|
||||
domain <- new.env(parent = emptyenv())
|
||||
domain$onEnded <- function(callback) {
|
||||
callbacks <<- c(callbacks, callback)
|
||||
return(callbacks$register(callback))
|
||||
}
|
||||
domain$isEnded <- function() {
|
||||
ended
|
||||
@@ -55,7 +55,7 @@ createMockDomain <- function() {
|
||||
domain$end <- function() {
|
||||
if (!ended) {
|
||||
ended <<- TRUE
|
||||
lapply(callbacks, do.call, list())
|
||||
callbacks$invoke()
|
||||
}
|
||||
invisible()
|
||||
}
|
||||
|
||||
@@ -714,12 +714,18 @@ Observer <- R6Class(
|
||||
.domain = 'ANY',
|
||||
.priority = numeric(0),
|
||||
.autoDestroy = logical(0),
|
||||
# A function that, when invoked, unsubscribes the autoDestroy
|
||||
# listener (or NULL if autodestroy is disabled for this observer).
|
||||
# We must unsubscribe when this observer is destroyed, or else
|
||||
# the observer cannot be garbage collected until the session ends.
|
||||
.autoDestroyHandle = 'ANY',
|
||||
.invalidateCallbacks = list(),
|
||||
.execCount = integer(0),
|
||||
.onResume = 'function',
|
||||
.suspended = logical(0),
|
||||
.destroyed = logical(0),
|
||||
.prevId = character(0),
|
||||
.ctx = NULL,
|
||||
|
||||
initialize = function(observerFunc, label, suspended = FALSE, priority = 0,
|
||||
domain = getDefaultReactiveDomain(),
|
||||
@@ -742,7 +748,6 @@ registerDebugHook("observerFunc", environment(), label)
|
||||
}
|
||||
.label <<- label
|
||||
.domain <<- domain
|
||||
.autoDestroy <<- autoDestroy
|
||||
.priority <<- normalizePriority(priority)
|
||||
.execCount <<- 0L
|
||||
.suspended <<- suspended
|
||||
@@ -750,7 +755,9 @@ registerDebugHook("observerFunc", environment(), label)
|
||||
.destroyed <<- FALSE
|
||||
.prevId <<- ''
|
||||
|
||||
onReactiveDomainEnded(.domain, self$.onDomainEnded)
|
||||
.autoDestroy <<- FALSE
|
||||
.autoDestroyHandle <<- NULL
|
||||
setAutoDestroy(autoDestroy)
|
||||
|
||||
# Defer the first running of this until flushReact is called
|
||||
.createContext()$invalidate()
|
||||
@@ -759,7 +766,23 @@ registerDebugHook("observerFunc", environment(), label)
|
||||
ctx <- Context$new(.domain, .label, type='observer', prevId=.prevId)
|
||||
.prevId <<- ctx$id
|
||||
|
||||
if (!is.null(.ctx)) {
|
||||
# If this happens, something went wrong.
|
||||
warning("Created a new context without invalidating previous context.")
|
||||
}
|
||||
# Store the context explicitly in the Observer object. This is necessary
|
||||
# to make sure that when the observer is destroyed, it also gets
|
||||
# invalidated. Otherwise the upstream reactive (on which the observer
|
||||
# depends) will hold a (indirect) reference to this context until the
|
||||
# reactive is invalidated, which may not happen immediately or at all.
|
||||
# This can lead to a memory leak (#1253).
|
||||
.ctx <<- ctx
|
||||
|
||||
ctx$onInvalidate(function() {
|
||||
# Context is invalidated, so we don't need to store a reference to it
|
||||
# anymore.
|
||||
.ctx <<- NULL
|
||||
|
||||
lapply(.invalidateCallbacks, function(invalidateCallback) {
|
||||
invalidateCallback()
|
||||
NULL
|
||||
@@ -812,11 +835,28 @@ registerDebugHook("observerFunc", environment(), label)
|
||||
"Sets whether this observer should be automatically destroyed when its
|
||||
domain (if any) ends. If autoDestroy is TRUE and the domain already
|
||||
ended, then destroy() is called immediately."
|
||||
|
||||
if (.autoDestroy == autoDestroy) {
|
||||
return(.autoDestroy)
|
||||
}
|
||||
|
||||
oldValue <- .autoDestroy
|
||||
.autoDestroy <<- autoDestroy
|
||||
if (!is.null(.domain) && .domain$isEnded()) {
|
||||
destroy()
|
||||
|
||||
if (autoDestroy) {
|
||||
if (!.destroyed && !is.null(.domain)) { # Make sure to not try to destroy twice.
|
||||
if (.domain$isEnded()) {
|
||||
destroy()
|
||||
} else {
|
||||
.autoDestroyHandle <<- onReactiveDomainEnded(.domain, .onDomainEnded)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!is.null(.autoDestroyHandle))
|
||||
.autoDestroyHandle()
|
||||
.autoDestroyHandle <<- NULL
|
||||
}
|
||||
|
||||
invisible(oldValue)
|
||||
},
|
||||
suspend = function() {
|
||||
@@ -842,8 +882,21 @@ registerDebugHook("observerFunc", environment(), label)
|
||||
"Prevents this observer from ever executing again (even if a flush has
|
||||
already been scheduled)."
|
||||
|
||||
# Make sure to not try to destory twice.
|
||||
if (.destroyed)
|
||||
return()
|
||||
|
||||
suspend()
|
||||
.destroyed <<- TRUE
|
||||
|
||||
if (!is.null(.autoDestroyHandle)) {
|
||||
.autoDestroyHandle()
|
||||
}
|
||||
.autoDestroyHandle <<- NULL
|
||||
|
||||
if (!is.null(.ctx)) {
|
||||
.ctx$invalidate()
|
||||
}
|
||||
},
|
||||
.onDomainEnded = function() {
|
||||
if (isTRUE(.autoDestroy)) {
|
||||
|
||||
37
R/server.R
37
R/server.R
@@ -254,7 +254,7 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
|
||||
if (length(splitName) > 1) {
|
||||
if (!inputHandlers$containsKey(splitName[[2]])) {
|
||||
# No input handler registered for this type
|
||||
stop("No handler registered for for type ", name)
|
||||
stop("No handler registered for type ", name)
|
||||
}
|
||||
|
||||
inputName <- splitName[[1]]
|
||||
@@ -592,7 +592,8 @@ runApp <- function(appDir=getwd(),
|
||||
host <- '0.0.0.0'
|
||||
|
||||
# Make warnings print immediately
|
||||
ops <- options(warn = 1)
|
||||
# Set pool.scheduler to support pool package
|
||||
ops <- options(warn = 1, pool.scheduler = scheduleTask)
|
||||
on.exit(options(ops), add = TRUE)
|
||||
|
||||
workerId(workerId)
|
||||
@@ -629,21 +630,38 @@ runApp <- function(appDir=getwd(),
|
||||
on.exit(close(con), add = TRUE)
|
||||
settings <- read.dcf(con)
|
||||
if ("DisplayMode" %in% colnames(settings)) {
|
||||
mode <- settings[1,"DisplayMode"]
|
||||
mode <- settings[1, "DisplayMode"]
|
||||
if (mode == "Showcase") {
|
||||
setShowcaseDefault(1)
|
||||
if ("IncludeWWW" %in% colnames(settings)) {
|
||||
.globals$IncludeWWW <- as.logical(settings[1, "IncludeWWW"])
|
||||
if (is.na(.globals$IncludeWWW)) {
|
||||
stop("In your Description file, `IncludeWWW` ",
|
||||
"must be set to `True` (default) or `False`")
|
||||
}
|
||||
} else {
|
||||
.globals$IncludeWWW <- TRUE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## default is to show the .js, .css and .html files in the www directory
|
||||
## (if not in showcase mode, this variable will simply be ignored)
|
||||
if (is.null(.globals$IncludeWWW) || is.na(.globals$IncludeWWW)) {
|
||||
.globals$IncludeWWW <- TRUE
|
||||
}
|
||||
|
||||
# If display mode is specified as an argument, apply it (overriding the
|
||||
# value specified in DESCRIPTION, if any).
|
||||
display.mode <- match.arg(display.mode)
|
||||
if (display.mode == "normal")
|
||||
if (display.mode == "normal") {
|
||||
setShowcaseDefault(0)
|
||||
else if (display.mode == "showcase")
|
||||
}
|
||||
else if (display.mode == "showcase") {
|
||||
setShowcaseDefault(1)
|
||||
}
|
||||
|
||||
require(shiny)
|
||||
|
||||
@@ -664,7 +682,14 @@ runApp <- function(appDir=getwd(),
|
||||
}
|
||||
else {
|
||||
# Try up to 20 random ports
|
||||
port <- p_randomInt(3000, 8000)
|
||||
while (TRUE) {
|
||||
port <- p_randomInt(3000, 8000)
|
||||
# Reject ports in this range that are considered unsafe by Chrome
|
||||
# http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
|
||||
if (!port %in% c(3659, 4045, 6000, 6665:6669)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Test port to see if we can use it
|
||||
|
||||
58
R/shiny.R
58
R/shiny.R
@@ -172,6 +172,18 @@ workerId <- local({
|
||||
#' example, \code{session$clientData$url_search}).
|
||||
#'
|
||||
#' @return
|
||||
#' \item{allowReconnect(value)}{
|
||||
#' If \code{value} is \code{TRUE} and run in a hosting environment (Shiny
|
||||
#' Server or Connect) with reconnections enabled, then when the session ends
|
||||
#' due to the network connection closing, the client will attempt to
|
||||
#' reconnect to the server. If a reconnection is successful, the browser will
|
||||
#' send all the current input values to the new session on the server, and
|
||||
#' the server will recalculate any outputs and send them back to the client.
|
||||
#' If \code{value} is \code{FALSE}, reconnections will be disabled (this is
|
||||
#' the default state). If \code{"force"}, then the client browser will always
|
||||
#' attempt to reconnect. The only reason to use \code{"force"} is for testing
|
||||
#' on a local connection (without Shiny Server or Connect).
|
||||
#' }
|
||||
#' \item{clientData}{
|
||||
#' A \code{\link{reactiveValues}} object that contains information about the client.
|
||||
#' \itemize{
|
||||
@@ -206,6 +218,11 @@ workerId <- local({
|
||||
#' \item{isClosed()}{A function that returns \code{TRUE} if the client has
|
||||
#' disconnected.
|
||||
#' }
|
||||
#' \item{ns(id)}{
|
||||
#' Server-side version of \code{ns <- \link{NS}(id)}. If bare IDs need to be
|
||||
#' explicitly namespaced for the current module, \code{session$ns("name")}
|
||||
#' will return the fully-qualified ID.
|
||||
#' }
|
||||
#' \item{onEnded(callback)}{
|
||||
#' Synonym for \code{onSessionEnded}.
|
||||
#' }
|
||||
@@ -253,17 +270,9 @@ workerId <- local({
|
||||
#' This is the request that was used to initiate the websocket connection
|
||||
#' (as opposed to the request that downloaded the web page for the app).
|
||||
#' }
|
||||
#' \item{allowReconnect(value)}{
|
||||
#' If \code{value} is \code{TRUE} and run in a hosting environment (Shiny
|
||||
#' Server or Connect) with reconnections enabled, then when the session ends
|
||||
#' due to the network connection closing, the client will attempt to
|
||||
#' reconnect to the server. If a reconnection is successful, the browser will
|
||||
#' send all the current input values to the new session on the server, and
|
||||
#' the server will recalculate any outputs and send them back to the client.
|
||||
#' If \code{value} is \code{FALSE}, reconnections will be disabled (this is
|
||||
#' the default state). If \code{"force"}, then the client browser will always
|
||||
#' attempt to reconnect. The only reason to use \code{"force"} is for testing
|
||||
#' on a local connection (without Shiny Server or Connect).
|
||||
#' \item{resetBrush(brushId)}{
|
||||
#' Resets/clears the brush with the given \code{brushId}, if it exists on
|
||||
#' any \code{imageOutput} or \code{plotOutput} in the app.
|
||||
#' }
|
||||
#' \item{sendCustomMessage(type, message)}{
|
||||
#' Sends a custom message to the web page. \code{type} must be a
|
||||
@@ -285,11 +294,6 @@ workerId <- local({
|
||||
#' from Shiny apps, but through friendlier wrapper functions like
|
||||
#' \code{\link{updateTextInput}}.
|
||||
#' }
|
||||
#' \item{ns(id)}{
|
||||
#' Server-side version of \code{ns <- \link{NS}(id)}. If bare IDs need to be
|
||||
#' explicitly namespaced for the current module, \code{session$ns("name")}
|
||||
#' will return the fully-qualified ID.
|
||||
#' }
|
||||
#'
|
||||
#' @name session
|
||||
NULL
|
||||
@@ -1122,15 +1126,13 @@ ShinySession <- R6Class(
|
||||
reload = function() {
|
||||
private$sendMessage(reload = TRUE)
|
||||
},
|
||||
sendInsertUI = function(selector, multiple, where,
|
||||
content, container) {
|
||||
sendInsertUI = function(selector, multiple, where, content) {
|
||||
private$sendMessage(
|
||||
`shiny-insert-ui` = list(
|
||||
selector = selector,
|
||||
multiple = multiple,
|
||||
where = where,
|
||||
content = content,
|
||||
container = container
|
||||
content = content
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -1145,6 +1147,13 @@ ShinySession <- R6Class(
|
||||
updateLocationBar = function(url) {
|
||||
private$sendMessage(updateLocationBar = list(url = url))
|
||||
},
|
||||
resetBrush = function(brushId) {
|
||||
private$sendMessage(
|
||||
resetBrush = list(
|
||||
brushId = brushId
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
# Public RPC methods
|
||||
`@uploadieFinish` = function() {
|
||||
@@ -1524,10 +1533,15 @@ ShinySession <- R6Class(
|
||||
#' @param ... Options to set for the output observer.
|
||||
#' @export
|
||||
outputOptions <- function(x, name, ...) {
|
||||
if (!inherits(x, "shinyoutput"))
|
||||
if (!inherits(x, "shinyoutput")) {
|
||||
stop("x must be a shinyoutput object.")
|
||||
}
|
||||
|
||||
name <- .subset2(x, 'ns')(name)
|
||||
if (!missing(name)) {
|
||||
name <- .subset2(x, 'ns')(name)
|
||||
} else {
|
||||
name <- NULL
|
||||
}
|
||||
|
||||
.subset2(x, 'impl')$outputOptions(name, ...)
|
||||
}
|
||||
|
||||
85
R/showcase.R
85
R/showcase.R
@@ -77,10 +77,60 @@ appMetadata <- function(desc) {
|
||||
else ""
|
||||
}
|
||||
|
||||
navTabsHelper <- function(files, prefix = "") {
|
||||
lapply(files, function(file) {
|
||||
with(tags,
|
||||
li(class=if (tolower(file) %in% c("app.r", "server.r")) "active" else "",
|
||||
a(href=paste("#", gsub(".", "_", file, fixed=TRUE), "_code", sep=""),
|
||||
"data-toggle"="tab", paste0(prefix, file)))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
navTabsDropdown <- function(files) {
|
||||
if (length(files) > 0) {
|
||||
with(tags,
|
||||
li(role="presentation", class="dropdown",
|
||||
a(class="dropdown-toggle", `data-toggle`="dropdown", href="#",
|
||||
role="button", `aria-haspopup`="true", `aria-expanded`="false",
|
||||
"www", span(class="caret")
|
||||
),
|
||||
ul(class="dropdown-menu", navTabsHelper(files))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tabContentHelper <- function(files, path, language) {
|
||||
lapply(files, function(file) {
|
||||
with(tags,
|
||||
div(class=paste("tab-pane",
|
||||
if (tolower(file) %in% c("app.r", "server.r")) " active"
|
||||
else "",
|
||||
sep=""),
|
||||
id=paste(gsub(".", "_", file, fixed=TRUE),
|
||||
"_code", sep=""),
|
||||
pre(class="shiny-code",
|
||||
# we need to prevent the indentation of <code> ... </code>
|
||||
HTML(format(tags$code(
|
||||
class=paste0("language-", language),
|
||||
paste(readUTF8(file.path.ci(path, file)), collapse="\n")
|
||||
), indent = FALSE))))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
# Returns tags containing the application's code in Bootstrap-style tabs in
|
||||
# showcase mode.
|
||||
showcaseCodeTabs <- function(codeLicense) {
|
||||
rFiles <- list.files(pattern = "\\.[rR]$")
|
||||
wwwFiles <- list()
|
||||
if (isTRUE(.globals$IncludeWWW)) {
|
||||
path <- file.path(getwd(), "www")
|
||||
wwwFiles$jsFiles <- list.files(path, pattern = "\\.js$")
|
||||
wwwFiles$cssFiles <- list.files(path, pattern = "\\.css$")
|
||||
wwwFiles$htmlFiles <- list.files(path, pattern = "\\.html$")
|
||||
}
|
||||
with(tags, div(id="showcase-code-tabs",
|
||||
a(id="showcase-code-position-toggle",
|
||||
class="btn btn-default btn-sm",
|
||||
@@ -88,27 +138,21 @@ showcaseCodeTabs <- function(codeLicense) {
|
||||
icon("level-up"),
|
||||
"show with app"),
|
||||
ul(class="nav nav-tabs",
|
||||
lapply(rFiles, function(rFile) {
|
||||
li(class=if (tolower(rFile) %in% c("app.r", "server.r")) "active" else "",
|
||||
a(href=paste("#", gsub(".", "_", rFile, fixed=TRUE),
|
||||
"_code", sep=""),
|
||||
"data-toggle"="tab", rFile))
|
||||
})),
|
||||
navTabsHelper(rFiles),
|
||||
navTabsDropdown(unlist(wwwFiles))
|
||||
),
|
||||
div(class="tab-content", id="showcase-code-content",
|
||||
lapply(rFiles, function(rFile) {
|
||||
div(class=paste("tab-pane",
|
||||
if (tolower(rFile) %in% c("app.r", "server.r")) " active"
|
||||
else "",
|
||||
sep=""),
|
||||
id=paste(gsub(".", "_", rFile, fixed=TRUE),
|
||||
"_code", sep=""),
|
||||
pre(class="shiny-code",
|
||||
# we need to prevent the indentation of <code> ... </code>
|
||||
HTML(format(tags$code(
|
||||
class="language-r",
|
||||
paste(readUTF8(file.path.ci(getwd(), rFile)), collapse="\n")
|
||||
), indent = FALSE))))
|
||||
})),
|
||||
tabContentHelper(rFiles, path = getwd(), language = "r"),
|
||||
tabContentHelper(wwwFiles$jsFiles,
|
||||
path = paste0(getwd(), "/www"),
|
||||
language = "javascript"),
|
||||
tabContentHelper(wwwFiles$cssFiles,
|
||||
path = paste0(getwd(), "/www"),
|
||||
language = "css"),
|
||||
tabContentHelper(wwwFiles$htmlFiles,
|
||||
path = paste0(getwd(), "/www"),
|
||||
language = "xml")
|
||||
),
|
||||
codeLicense))
|
||||
}
|
||||
|
||||
@@ -177,3 +221,4 @@ showcaseUI <- function(ui) {
|
||||
showcaseBody(ui)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
13
R/timer.R
13
R/timer.R
@@ -71,3 +71,16 @@ TimerCallbacks <- R6Class(
|
||||
)
|
||||
|
||||
timerCallbacks <- TimerCallbacks$new()
|
||||
|
||||
scheduleTask <- function(millis, callback) {
|
||||
cancelled <- FALSE
|
||||
timerCallbacks$schedule(millis, function() {
|
||||
if (!cancelled)
|
||||
callback()
|
||||
})
|
||||
|
||||
function() {
|
||||
cancelled <<- TRUE
|
||||
callback <<- NULL # to allow for callback to be gc'ed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,7 +634,10 @@ Callbacks <- R6Class(
|
||||
.callbacks = 'Map',
|
||||
|
||||
initialize = function() {
|
||||
.nextId <<- as.integer(.Machine$integer.max)
|
||||
# NOTE: we avoid using '.Machine$integer.max' directly
|
||||
# as R 3.3.0's 'radixsort' could segfault when sorting
|
||||
# an integer vector containing this value
|
||||
.nextId <<- as.integer(.Machine$integer.max - 1L)
|
||||
.callbacks <<- Map$new()
|
||||
},
|
||||
register = function(callback) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,7 @@
|
||||
code {
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
pre .operator,
|
||||
pre .paren {
|
||||
color: rgb(104, 118, 135)
|
||||
@@ -13,9 +17,11 @@ pre .number {
|
||||
|
||||
pre .comment {
|
||||
color: rgb(76, 136, 107);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
pre .keyword {
|
||||
pre .keyword,
|
||||
pre .id {
|
||||
color: rgb(0, 0, 255);
|
||||
}
|
||||
|
||||
@@ -23,7 +29,53 @@ pre .identifier {
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
pre .string {
|
||||
pre .string,
|
||||
pre .attribute {
|
||||
color: rgb(3, 106, 7);
|
||||
}
|
||||
|
||||
pre .doctype {
|
||||
color: rgb(104, 104, 92);
|
||||
}
|
||||
|
||||
pre .tag,
|
||||
pre .title {
|
||||
color: rgb(4, 29, 140);
|
||||
}
|
||||
|
||||
pre .value {
|
||||
color: rgb(13, 105, 18);
|
||||
}
|
||||
|
||||
.language-xml .attribute {
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.language-css .attribute {
|
||||
color: rgb(110, 124, 219);
|
||||
}
|
||||
|
||||
.language-css .value {
|
||||
color: rgb(23, 149, 30);
|
||||
}
|
||||
|
||||
.language-css .number,
|
||||
.language-css .hexcolor {
|
||||
color: rgb(7, 27, 201);
|
||||
}
|
||||
|
||||
.language-css .function {
|
||||
color: rgb(61, 77, 113);
|
||||
}
|
||||
|
||||
.language-css .tag {
|
||||
color: rgb(195, 13, 25);
|
||||
}
|
||||
|
||||
.language-css .class {
|
||||
color: rgb(53, 132, 148);
|
||||
}
|
||||
|
||||
.language-css .pseudo {
|
||||
color: rgb(13, 105, 18);
|
||||
}
|
||||
|
||||
@@ -54,13 +54,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
// If this is not a text node, descend recursively to see how many
|
||||
// If this is not a text node, descend recursively to see how many
|
||||
// lines it contains.
|
||||
else if (child.nodeType === 1) { // ELEMENT_NODE
|
||||
var ret = findTextPoint(child, line - newlines, col);
|
||||
if (ret.element !== null)
|
||||
return ret;
|
||||
else
|
||||
return ret;
|
||||
else
|
||||
newlines += ret.offset;
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@
|
||||
var code = document.getElementById(srcfile.replace(/\./g, "_") + "_code");
|
||||
var start = findTextPoint(code, ref[0], ref[4]);
|
||||
var end = findTextPoint(code, ref[2], ref[5]);
|
||||
|
||||
|
||||
// If the insertion point can't be found, bail out now
|
||||
if (start.element === null || end.element === null)
|
||||
return;
|
||||
@@ -129,10 +129,10 @@
|
||||
var animateCodeMs = animate ? animateMs : 1;
|
||||
|
||||
// set the source and targets for the tab move
|
||||
var newHostElement = above ?
|
||||
var newHostElement = above ?
|
||||
document.getElementById("showcase-sxs-code") :
|
||||
document.getElementById("showcase-code-inline");
|
||||
var currentHostElement = above ?
|
||||
var currentHostElement = above ?
|
||||
document.getElementById("showcase-code-inline") :
|
||||
document.getElementById("showcase-sxs-code");
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
|
||||
$(newHostElement).fadeIn();
|
||||
if (!above) {
|
||||
// remove the applied width and zoom on the app container, and
|
||||
// remove the applied width and zoom on the app container, and
|
||||
// scroll smoothly down to the code's new home
|
||||
document.getElementById("showcase-app-container").removeAttribute("style");
|
||||
if (animate)
|
||||
@@ -234,9 +234,9 @@
|
||||
};
|
||||
|
||||
// make the code scrollable to about the height of the browser, less space
|
||||
// for the tabs
|
||||
// for the tabs
|
||||
var setCodeHeightFromDocHeight = function() {
|
||||
document.getElementById("showcase-code-content").style.height =
|
||||
document.getElementById("showcase-code-content").style.height =
|
||||
$(window).height() + "px";
|
||||
};
|
||||
|
||||
@@ -247,7 +247,7 @@
|
||||
// IE8 puts the content of <script> tags into innerHTML but
|
||||
// not innerText
|
||||
var content = mdContent.innerText || mdContent.innerHTML;
|
||||
document.getElementById("readme-md").innerHTML =
|
||||
document.getElementById("readme-md").innerHTML =
|
||||
(new Showdown.converter()).makeHtml(content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,46 +124,17 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.shiny-progress-container {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
/* Make sure it draws above all Bootstrap components */
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.shiny-progress .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
height: 3px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.shiny-progress .bar {
|
||||
opacity: 0.6;
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
height: 24px;
|
||||
width: 240px;
|
||||
background-color: #eef8ff;
|
||||
margin: 0px;
|
||||
padding: 2px 3px;
|
||||
opacity: 0.85;
|
||||
margin-bottom: 5px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-message {
|
||||
padding: 0px 3px;
|
||||
font-weight: bold;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.shiny-progress .progress-text .progress-detail {
|
||||
padding: 0px 3px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
|
||||
@@ -200,30 +200,6 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
|
||||
return val.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g, '\\$1');
|
||||
};
|
||||
|
||||
// Helper function for addMessageHandler('shiny-insert-ui').
|
||||
// Turns out that Firefox does not support insertAdjacentElement().
|
||||
// So we have to implement our own version for insertUI.
|
||||
function insertAdjacentElement(where, element, content) {
|
||||
switch (where) {
|
||||
case 'beforeBegin':
|
||||
element.parentNode.insertBefore(content, element);
|
||||
break;
|
||||
case 'afterBegin':
|
||||
element.insertBefore(content, element.firstChild);
|
||||
break;
|
||||
case 'beforeEnd':
|
||||
element.appendChild(content);
|
||||
break;
|
||||
case 'afterEnd':
|
||||
if (element.nextSibling) {
|
||||
element.parentNode.insertBefore(content, element.nextSibling);
|
||||
} else {
|
||||
element.parentNode.appendChild(content);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Source file: ../srcjs/browser.js
|
||||
|
||||
@@ -1168,9 +1144,7 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
|
||||
exports.renderHtml($([]), message.content.html, message.content.deps);
|
||||
} else {
|
||||
targets.each(function (i, target) {
|
||||
var container = document.createElement(message.container);
|
||||
insertAdjacentElement(message.where, target, container);
|
||||
exports.renderContent(container, message.content);
|
||||
exports.renderContent(target, message.content, message.where);
|
||||
return message.multiple;
|
||||
});
|
||||
}
|
||||
@@ -1192,6 +1166,10 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
|
||||
window.history.replaceState(null, null, message.url);
|
||||
});
|
||||
|
||||
addMessageHandler("resetBrush", function (message) {
|
||||
exports.resetBrush(message.brushId);
|
||||
});
|
||||
|
||||
// Progress reporting ====================================================
|
||||
|
||||
var progressHandlers = {
|
||||
@@ -1203,35 +1181,24 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
|
||||
binding.showProgress(true);
|
||||
}
|
||||
},
|
||||
|
||||
// Open a page-level progress bar
|
||||
open: function open(message) {
|
||||
// Add progress container (for all progress items) if not already present
|
||||
var $container = $('.shiny-progress-container');
|
||||
if ($container.length === 0) {
|
||||
$container = $('<div class="shiny-progress-container"></div>');
|
||||
$('body').append($container);
|
||||
}
|
||||
|
||||
// Add div for just this progress ID
|
||||
var depth = $('.shiny-progress.open').length;
|
||||
var $progress = $(progressHandlers.progressHTML);
|
||||
$progress.attr('id', message.id);
|
||||
$container.append($progress);
|
||||
|
||||
// Stack bars
|
||||
var $progressBar = $progress.find('.progress');
|
||||
$progressBar.css('top', depth * $progressBar.height() + 'px');
|
||||
|
||||
// Stack text objects
|
||||
var $progressText = $progress.find('.progress-text');
|
||||
$progressText.css('top', 3 * $progressBar.height() + depth * $progressText.outerHeight() + 'px');
|
||||
|
||||
$progress.hide();
|
||||
// Progress bar starts hidden; will be made visible if a value is provided
|
||||
// during updates.
|
||||
exports.notifications.show({
|
||||
html: '<div id="shiny-progress-' + message.id + '" class="shiny-progress">' + '<div class="progress progress-striped active" style="display: none;"><div class="progress-bar"></div></div>' + '<div class="progress-text">' + '<span class="progress-message">message</span> ' + '<span class="progress-detail"></span>' + '</div>' + '</div>',
|
||||
id: message.id,
|
||||
duration: null
|
||||
});
|
||||
},
|
||||
|
||||
// Update page-level progress bar
|
||||
update: function update(message) {
|
||||
var $progress = $('#' + message.id + '.shiny-progress');
|
||||
var $progress = $('#shiny-progress-' + message.id);
|
||||
|
||||
if ($progress.length === 0) return;
|
||||
|
||||
if (typeof message.message !== 'undefined') {
|
||||
$progress.find('.progress-message').text(message.message);
|
||||
}
|
||||
@@ -1241,32 +1208,17 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
|
||||
if (typeof message.value !== 'undefined') {
|
||||
if (message.value !== null) {
|
||||
$progress.find('.progress').show();
|
||||
$progress.find('.bar').width(message.value * 100 + '%');
|
||||
$progress.find('.progress-bar').width(message.value * 100 + '%');
|
||||
} else {
|
||||
$progress.find('.progress').hide();
|
||||
}
|
||||
}
|
||||
|
||||
$progress.fadeIn();
|
||||
},
|
||||
|
||||
// Close page-level progress bar
|
||||
close: function close(message) {
|
||||
var $progress = $('#' + message.id + '.shiny-progress');
|
||||
$progress.removeClass('open');
|
||||
|
||||
$progress.fadeOut({
|
||||
complete: function complete() {
|
||||
$progress.remove();
|
||||
|
||||
// If this was the last shiny-progress, remove container
|
||||
if ($('.shiny-progress').length === 0) $('.shiny-progress-container').remove();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// The 'bar' class is needed for backward compatibility with Bootstrap 2.
|
||||
progressHTML: '<div class="shiny-progress open">' + '<div class="progress progress-striped active"><div class="progress-bar bar"></div></div>' + '<div class="progress-text">' + '<span class="progress-message">message</span>' + '<span class="progress-detail"></span>' + '</div>' + '</div>'
|
||||
exports.notifications.remove(message.id);
|
||||
}
|
||||
};
|
||||
|
||||
exports.progressHandlers = progressHandlers;
|
||||
@@ -3003,6 +2955,13 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
|
||||
};
|
||||
};
|
||||
|
||||
exports.resetBrush = function (brushId) {
|
||||
exports.onInputChange(brushId, null);
|
||||
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
|
||||
brushId: brushId, outputId: null
|
||||
});
|
||||
};
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Source file: ../srcjs/output_binding_html.js
|
||||
|
||||
@@ -3033,6 +2992,8 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
|
||||
// inputs/outputs. `content` can be null, a string, or an object with
|
||||
// properties 'html' and 'deps'.
|
||||
exports.renderContent = function (el, content) {
|
||||
var where = arguments.length <= 2 || arguments[2] === undefined ? "replace" : arguments[2];
|
||||
|
||||
exports.unbindAll(el);
|
||||
|
||||
var html;
|
||||
@@ -3046,15 +3007,32 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
|
||||
dependencies = content.deps || [];
|
||||
}
|
||||
|
||||
exports.renderHtml(html, el, dependencies);
|
||||
exports.initializeInputs(el);
|
||||
exports.bindAll(el);
|
||||
exports.renderHtml(html, el, dependencies, where);
|
||||
|
||||
var scope = el;
|
||||
if (where === "replace") {
|
||||
exports.initializeInputs(el);
|
||||
exports.bindAll(el);
|
||||
} else {
|
||||
var $parent = $(el).parent();
|
||||
if ($parent.length > 0) {
|
||||
scope = $parent;
|
||||
if (where === "beforeBegin" || where === "afterEnd") {
|
||||
var $grandparent = $parent.parent();
|
||||
if ($grandparent.length > 0) scope = $grandparent;
|
||||
}
|
||||
}
|
||||
exports.initializeInputs(scope);
|
||||
exports.bindAll(scope);
|
||||
}
|
||||
};
|
||||
|
||||
// Render HTML in a DOM element, inserting singletons into head as needed
|
||||
exports.renderHtml = function (html, el, dependencies) {
|
||||
var where = arguments.length <= 3 || arguments[3] === undefined ? 'replace' : arguments[3];
|
||||
|
||||
renderDependencies(dependencies);
|
||||
return singletons.renderHtml(html, el);
|
||||
return singletons.renderHtml(html, el, where);
|
||||
};
|
||||
|
||||
var htmlDependencies = {};
|
||||
@@ -3123,11 +3101,15 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
|
||||
|
||||
var singletons = {
|
||||
knownSingletons: {},
|
||||
renderHtml: function renderHtml(html, el) {
|
||||
renderHtml: function renderHtml(html, el, where) {
|
||||
var processed = this._processHtml(html);
|
||||
this._addToHead(processed.head);
|
||||
this.register(processed.singletons);
|
||||
$(el).html(processed.html);
|
||||
if (where === "replace") {
|
||||
$(el).html(processed.html);
|
||||
} else {
|
||||
el.insertAdjacentHTML(where, processed.html);
|
||||
}
|
||||
return processed;
|
||||
},
|
||||
// Take an object where keys are names of singletons, and merges it into
|
||||
@@ -4960,7 +4942,10 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
|
||||
|
||||
// Iterate over all input objects for this binding
|
||||
for (var j = 0; j < inputObjects.length; j++) {
|
||||
binding.initialize(inputObjects[j]);
|
||||
if (!inputObjects[j]._shiny_initialized) {
|
||||
inputObjects[j]._shiny_initialized = true;
|
||||
binding.initialize(inputObjects[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
8
inst/www/shared/shiny.min.js
vendored
8
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,9 +4,9 @@
|
||||
\alias{insertUI}
|
||||
\title{Insert UI objects}
|
||||
\usage{
|
||||
insertUI(selector, multiple = FALSE, where = c("beforeBegin", "afterBegin",
|
||||
"beforeEnd", "afterEnd"), ui, immediate = FALSE, container = if (inline)
|
||||
"span" else "div", inline = FALSE, session = getDefaultReactiveDomain())
|
||||
insertUI(selector, where = c("beforeBegin", "afterBegin", "beforeEnd",
|
||||
"afterEnd"), ui, multiple = FALSE, immediate = FALSE,
|
||||
session = getDefaultReactiveDomain())
|
||||
}
|
||||
\arguments{
|
||||
\item{selector}{A string that is accepted by jQuery's selector (i.e. the
|
||||
@@ -14,11 +14,6 @@ string \code{s} to be placed in a \code{$(s)} jQuery call). This selector
|
||||
will determine the element(s) relative to which you want to insert your
|
||||
UI object.}
|
||||
|
||||
\item{multiple}{In case your selector matches more than one element,
|
||||
\code{multiple} determines whether Shiny should insert the UI object
|
||||
relative to all matched elements or just relative to the first
|
||||
matched element (default).}
|
||||
|
||||
\item{where}{Where your UI object should go relative to the selector:
|
||||
\describe{
|
||||
\item{\code{beforeBegin}}{Before the selector element itself}
|
||||
@@ -32,18 +27,22 @@ Adapted from
|
||||
\href{https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML}{here}.}
|
||||
|
||||
\item{ui}{The UI object you want to insert. This can be anything that
|
||||
you usually put inside your apps's \code{ui} function.}
|
||||
you usually put inside your apps's \code{ui} function. If you're inserting
|
||||
multiple elements in one call, make sure to wrap them in either a
|
||||
\code{tagList()} or a \code{tags$div()} (the latter option has the
|
||||
advantage that you can give it an \code{id} to make it easier to
|
||||
reference or remove it later on). If you want to insert raw html, use
|
||||
\code{ui = HTML()}.}
|
||||
|
||||
\item{multiple}{In case your selector matches more than one element,
|
||||
\code{multiple} determines whether Shiny should insert the UI object
|
||||
relative to all matched elements or just relative to the first
|
||||
matched element (default).}
|
||||
|
||||
\item{immediate}{Whether the UI object should be immediately inserted into
|
||||
the app when you call \code{insertUI}, or whether Shiny should wait until
|
||||
all outputs have been updated and all observers have been run (default).}
|
||||
|
||||
\item{container}{A function to generate an HTML element to contain the UI
|
||||
object.}
|
||||
|
||||
\item{inline}{Use an inline (\code{span()}) or block container (\code{div()},
|
||||
default) for the output.}
|
||||
|
||||
\item{session}{The shiny session within which to call \code{insertUI}.}
|
||||
}
|
||||
\description{
|
||||
@@ -59,12 +58,6 @@ the ones already there (all independent from one another). To
|
||||
update a part of the UI (ex: an input object), you must use the
|
||||
appropriate \code{render} function or a customized \code{reactive}
|
||||
function. To remove any part of your UI, use \code{\link{removeUI}}.
|
||||
|
||||
Note that whatever UI object you pass through \code{ui}, it is always
|
||||
wrapped in an extra \code{div} (or if \code{inline = TRUE}, a
|
||||
\code{span}) before making its way into the DOM. This does not affect
|
||||
what you mean to do, and it makes it easier to remove the whole UI
|
||||
object using \code{\link{removeUI}} (if you wish to do so, of course).
|
||||
}
|
||||
\examples{
|
||||
## Only run this example in interactive R sessions
|
||||
|
||||
@@ -12,7 +12,9 @@ removeUI(selector, multiple = FALSE, immediate = FALSE,
|
||||
string \code{s} to be placed in a \code{$(s)} jQuery call). This selector
|
||||
will determine the element(s) to be removed. If you want to remove a
|
||||
Shiny input or output, note that many of these are wrapped in \code{div}s,
|
||||
so you may need to use a somewhat complex selector (see the Examples below).}
|
||||
so you may need to use a somewhat complex selector -- see the Examples below.
|
||||
(Alternatively, you could also wrap the inputs/outputs that you want to be
|
||||
able to remove easily in a \code{div} with an id.)}
|
||||
|
||||
\item{multiple}{In case your selector matches more than one element,
|
||||
\code{multiple} determines whether Shiny should remove all the matched
|
||||
|
||||
@@ -4,6 +4,18 @@
|
||||
\alias{session}
|
||||
\title{Session object}
|
||||
\value{
|
||||
\item{allowReconnect(value)}{
|
||||
If \code{value} is \code{TRUE} and run in a hosting environment (Shiny
|
||||
Server or Connect) with reconnections enabled, then when the session ends
|
||||
due to the network connection closing, the client will attempt to
|
||||
reconnect to the server. If a reconnection is successful, the browser will
|
||||
send all the current input values to the new session on the server, and
|
||||
the server will recalculate any outputs and send them back to the client.
|
||||
If \code{value} is \code{FALSE}, reconnections will be disabled (this is
|
||||
the default state). If \code{"force"}, then the client browser will always
|
||||
attempt to reconnect. The only reason to use \code{"force"} is for testing
|
||||
on a local connection (without Shiny Server or Connect).
|
||||
}
|
||||
\item{clientData}{
|
||||
A \code{\link{reactiveValues}} object that contains information about the client.
|
||||
\itemize{
|
||||
@@ -38,6 +50,11 @@
|
||||
\item{isClosed()}{A function that returns \code{TRUE} if the client has
|
||||
disconnected.
|
||||
}
|
||||
\item{ns(id)}{
|
||||
Server-side version of \code{ns <- \link{NS}(id)}. If bare IDs need to be
|
||||
explicitly namespaced for the current module, \code{session$ns("name")}
|
||||
will return the fully-qualified ID.
|
||||
}
|
||||
\item{onEnded(callback)}{
|
||||
Synonym for \code{onSessionEnded}.
|
||||
}
|
||||
@@ -85,17 +102,9 @@
|
||||
This is the request that was used to initiate the websocket connection
|
||||
(as opposed to the request that downloaded the web page for the app).
|
||||
}
|
||||
\item{allowReconnect(value)}{
|
||||
If \code{value} is \code{TRUE} and run in a hosting environment (Shiny
|
||||
Server or Connect) with reconnections enabled, then when the session ends
|
||||
due to the network connection closing, the client will attempt to
|
||||
reconnect to the server. If a reconnection is successful, the browser will
|
||||
send all the current input values to the new session on the server, and
|
||||
the server will recalculate any outputs and send them back to the client.
|
||||
If \code{value} is \code{FALSE}, reconnections will be disabled (this is
|
||||
the default state). If \code{"force"}, then the client browser will always
|
||||
attempt to reconnect. The only reason to use \code{"force"} is for testing
|
||||
on a local connection (without Shiny Server or Connect).
|
||||
\item{resetBrush(brushId)}{
|
||||
Resets/clears the brush with the given \code{brushId}, if it exists on
|
||||
any \code{imageOutput} or \code{plotOutput} in the app.
|
||||
}
|
||||
\item{sendCustomMessage(type, message)}{
|
||||
Sends a custom message to the web page. \code{type} must be a
|
||||
@@ -117,11 +126,6 @@
|
||||
from Shiny apps, but through friendlier wrapper functions like
|
||||
\code{\link{updateTextInput}}.
|
||||
}
|
||||
\item{ns(id)}{
|
||||
Server-side version of \code{ns <- \link{NS}(id)}. If bare IDs need to be
|
||||
explicitly namespaced for the current module, \code{session$ns("name")}
|
||||
will return the fully-qualified ID.
|
||||
}
|
||||
}
|
||||
\description{
|
||||
Shiny server functions can optionally include \code{session} as a parameter
|
||||
|
||||
@@ -225,7 +225,10 @@ function initShiny() {
|
||||
|
||||
// Iterate over all input objects for this binding
|
||||
for (var j = 0; j < inputObjects.length; j++) {
|
||||
binding.initialize(inputObjects[j]);
|
||||
if (!inputObjects[j]._shiny_initialized) {
|
||||
inputObjects[j]._shiny_initialized = true;
|
||||
binding.initialize(inputObjects[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ var renderDependencies = exports.renderDependencies = function(dependencies) {
|
||||
// Render HTML in a DOM element, add dependencies, and bind Shiny
|
||||
// inputs/outputs. `content` can be null, a string, or an object with
|
||||
// properties 'html' and 'deps'.
|
||||
exports.renderContent = function(el, content) {
|
||||
exports.renderContent = function(el, content, where="replace") {
|
||||
exports.unbindAll(el);
|
||||
|
||||
var html;
|
||||
@@ -38,15 +38,30 @@ exports.renderContent = function(el, content) {
|
||||
dependencies = content.deps || [];
|
||||
}
|
||||
|
||||
exports.renderHtml(html, el, dependencies);
|
||||
exports.initializeInputs(el);
|
||||
exports.bindAll(el);
|
||||
exports.renderHtml(html, el, dependencies, where);
|
||||
|
||||
var scope = el;
|
||||
if (where === "replace") {
|
||||
exports.initializeInputs(el);
|
||||
exports.bindAll(el);
|
||||
} else {
|
||||
var $parent = $(el).parent();
|
||||
if ($parent.length > 0) {
|
||||
scope = $parent;
|
||||
if (where === "beforeBegin" || where === "afterEnd") {
|
||||
var $grandparent = $parent.parent();
|
||||
if ($grandparent.length > 0) scope = $grandparent;
|
||||
}
|
||||
}
|
||||
exports.initializeInputs(scope);
|
||||
exports.bindAll(scope);
|
||||
}
|
||||
};
|
||||
|
||||
// Render HTML in a DOM element, inserting singletons into head as needed
|
||||
exports.renderHtml = function(html, el, dependencies) {
|
||||
exports.renderHtml = function(html, el, dependencies, where = 'replace') {
|
||||
renderDependencies(dependencies);
|
||||
return singletons.renderHtml(html, el);
|
||||
return singletons.renderHtml(html, el, where);
|
||||
};
|
||||
|
||||
var htmlDependencies = {};
|
||||
@@ -120,11 +135,15 @@ function renderDependency(dep) {
|
||||
|
||||
var singletons = {
|
||||
knownSingletons: {},
|
||||
renderHtml: function(html, el) {
|
||||
renderHtml: function(html, el, where) {
|
||||
var processed = this._processHtml(html);
|
||||
this._addToHead(processed.head);
|
||||
this.register(processed.singletons);
|
||||
$(el).html(processed.html);
|
||||
if (where === "replace") {
|
||||
$(el).html(processed.html);
|
||||
} else {
|
||||
el.insertAdjacentHTML(where, processed.html);
|
||||
}
|
||||
return processed;
|
||||
},
|
||||
// Take an object where keys are names of singletons, and merges it into
|
||||
|
||||
@@ -1368,3 +1368,10 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) {
|
||||
stopResizing: stopResizing
|
||||
};
|
||||
};
|
||||
|
||||
exports.resetBrush = function(brushId) {
|
||||
exports.onInputChange(brushId, null);
|
||||
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
|
||||
brushId: brushId, outputId: null
|
||||
});
|
||||
};
|
||||
|
||||
@@ -641,21 +641,19 @@ var ShinyApp = function() {
|
||||
});
|
||||
|
||||
addMessageHandler('shiny-insert-ui', function (message) {
|
||||
var targets = $(message.selector);
|
||||
if (targets.length === 0) {
|
||||
// render the HTML and deps to a null target, so
|
||||
// the side-effect of rendering the deps, singletons,
|
||||
// and <head> still occur
|
||||
exports.renderHtml($([]), message.content.html, message.content.deps);
|
||||
} else {
|
||||
targets.each(function (i, target) {
|
||||
var container = document.createElement(message.container);
|
||||
insertAdjacentElement(message.where, target, container);
|
||||
exports.renderContent(container, message.content);
|
||||
return message.multiple;
|
||||
});
|
||||
}
|
||||
});
|
||||
var targets = $(message.selector);
|
||||
if (targets.length === 0) {
|
||||
// render the HTML and deps to a null target, so
|
||||
// the side-effect of rendering the deps, singletons,
|
||||
// and <head> still occur
|
||||
exports.renderHtml($([]), message.content.html, message.content.deps);
|
||||
} else {
|
||||
targets.each(function (i, target) {
|
||||
exports.renderContent(target, message.content, message.where);
|
||||
return message.multiple;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addMessageHandler('shiny-remove-ui', function (message) {
|
||||
var els = $(message.selector);
|
||||
@@ -673,6 +671,9 @@ var ShinyApp = function() {
|
||||
window.history.replaceState(null, null, message.url);
|
||||
});
|
||||
|
||||
addMessageHandler("resetBrush", function(message) {
|
||||
exports.resetBrush(message.brushId);
|
||||
});
|
||||
|
||||
// Progress reporting ====================================================
|
||||
|
||||
@@ -685,36 +686,32 @@ var ShinyApp = function() {
|
||||
binding.showProgress(true);
|
||||
}
|
||||
},
|
||||
|
||||
// Open a page-level progress bar
|
||||
open: function(message) {
|
||||
// Add progress container (for all progress items) if not already present
|
||||
var $container = $('.shiny-progress-container');
|
||||
if ($container.length === 0) {
|
||||
$container = $('<div class="shiny-progress-container"></div>');
|
||||
$('body').append($container);
|
||||
}
|
||||
|
||||
// Add div for just this progress ID
|
||||
var depth = $('.shiny-progress.open').length;
|
||||
var $progress = $(progressHandlers.progressHTML);
|
||||
$progress.attr('id', message.id);
|
||||
$container.append($progress);
|
||||
|
||||
// Stack bars
|
||||
var $progressBar = $progress.find('.progress');
|
||||
$progressBar.css('top', depth * $progressBar.height() + 'px');
|
||||
|
||||
// Stack text objects
|
||||
var $progressText = $progress.find('.progress-text');
|
||||
$progressText.css('top', 3 * $progressBar.height() +
|
||||
depth * $progressText.outerHeight() + 'px');
|
||||
|
||||
$progress.hide();
|
||||
// Progress bar starts hidden; will be made visible if a value is provided
|
||||
// during updates.
|
||||
exports.notifications.show({
|
||||
html:
|
||||
`<div id="shiny-progress-${message.id}" class="shiny-progress">` +
|
||||
'<div class="progress progress-striped active" style="display: none;"><div class="progress-bar"></div></div>' +
|
||||
'<div class="progress-text">' +
|
||||
'<span class="progress-message">message</span> ' +
|
||||
'<span class="progress-detail"></span>' +
|
||||
'</div>' +
|
||||
'</div>',
|
||||
id: message.id,
|
||||
duration: null
|
||||
});
|
||||
},
|
||||
|
||||
// Update page-level progress bar
|
||||
update: function(message) {
|
||||
var $progress = $('#' + message.id + '.shiny-progress');
|
||||
var $progress = $('#shiny-progress-' + message.id);
|
||||
|
||||
if ($progress.length === 0)
|
||||
return;
|
||||
|
||||
if (typeof(message.message) !== 'undefined') {
|
||||
$progress.find('.progress-message').text(message.message);
|
||||
}
|
||||
@@ -724,40 +721,18 @@ var ShinyApp = function() {
|
||||
if (typeof(message.value) !== 'undefined') {
|
||||
if (message.value !== null) {
|
||||
$progress.find('.progress').show();
|
||||
$progress.find('.bar').width((message.value*100) + '%');
|
||||
}
|
||||
else {
|
||||
$progress.find('.progress-bar').width((message.value*100) + '%');
|
||||
|
||||
} else {
|
||||
$progress.find('.progress').hide();
|
||||
}
|
||||
}
|
||||
|
||||
$progress.fadeIn();
|
||||
},
|
||||
|
||||
// Close page-level progress bar
|
||||
close: function(message) {
|
||||
var $progress = $('#' + message.id + '.shiny-progress');
|
||||
$progress.removeClass('open');
|
||||
|
||||
$progress.fadeOut({
|
||||
complete: function() {
|
||||
$progress.remove();
|
||||
|
||||
// If this was the last shiny-progress, remove container
|
||||
if ($('.shiny-progress').length === 0)
|
||||
$('.shiny-progress-container').remove();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// The 'bar' class is needed for backward compatibility with Bootstrap 2.
|
||||
progressHTML: '<div class="shiny-progress open">' +
|
||||
'<div class="progress progress-striped active"><div class="progress-bar bar"></div></div>' +
|
||||
'<div class="progress-text">' +
|
||||
'<span class="progress-message">message</span>' +
|
||||
'<span class="progress-detail"></span>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
exports.notifications.remove(message.id);
|
||||
}
|
||||
};
|
||||
|
||||
exports.progressHandlers = progressHandlers;
|
||||
|
||||
@@ -201,28 +201,3 @@ function mergeSort(list, sortfunc) {
|
||||
var $escape = exports.$escape = function(val) {
|
||||
return val.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g, '\\$1');
|
||||
};
|
||||
|
||||
|
||||
// Helper function for addMessageHandler('shiny-insert-ui').
|
||||
// Turns out that Firefox does not support insertAdjacentElement().
|
||||
// So we have to implement our own version for insertUI.
|
||||
function insertAdjacentElement(where, element, content) {
|
||||
switch (where) {
|
||||
case 'beforeBegin':
|
||||
element.parentNode.insertBefore(content, element);
|
||||
break;
|
||||
case 'afterBegin':
|
||||
element.insertBefore(content, element.firstChild);
|
||||
break;
|
||||
case 'beforeEnd':
|
||||
element.appendChild(content);
|
||||
break;
|
||||
case 'afterEnd':
|
||||
if (element.nextSibling) {
|
||||
element.parentNode.insertBefore(content, element.nextSibling);
|
||||
} else {
|
||||
element.parentNode.appendChild(content);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,6 +819,66 @@ test_that("observers autodestroy (or not)", {
|
||||
})
|
||||
})
|
||||
|
||||
test_that("observers are garbage collected when destroyed", {
|
||||
domain <- createMockDomain()
|
||||
rv <- reactiveValues(x = 1)
|
||||
|
||||
# Auto-destroy. GC on domain end.
|
||||
a <- observe(rv$x, domain = domain)
|
||||
# No auto-destroy. GC with rv.
|
||||
b <- observe(rv$x, domain = domain, autoDestroy = FALSE)
|
||||
# No auto-destroy and no reactive dependencies. GC immediately.
|
||||
c <- observe({}, domain = domain)
|
||||
c$setAutoDestroy(FALSE)
|
||||
# Similar to b, but we'll set it to autoDestroy later.
|
||||
d <- observe(rv$x, domain = domain, autoDestroy = FALSE)
|
||||
# Like a, but we'll destroy it immediately.
|
||||
e <- observe(rx$x, domain = domain)
|
||||
e$destroy()
|
||||
|
||||
collected <- new.env(parent = emptyenv())
|
||||
|
||||
reg.finalizer(a, function(o) collected$a <- TRUE)
|
||||
reg.finalizer(b, function(o) collected$b <- TRUE)
|
||||
reg.finalizer(c, function(o) collected$c <- TRUE)
|
||||
reg.finalizer(d, function(o) collected$d <- TRUE)
|
||||
reg.finalizer(e, function(o) collected$e <- TRUE)
|
||||
|
||||
rm(list = c("a", "b", "c", "e")) # Not "d"
|
||||
|
||||
gc()
|
||||
# Nothing can be GC'd yet, because all of the observers are
|
||||
# pending execution (i.e. waiting for flushReact).
|
||||
expect_equal(ls(collected), character())
|
||||
|
||||
flushReact()
|
||||
# Now "c" can be garbage collected, because it ran and took
|
||||
# no dependencies (and isn't tied to the session in any way).
|
||||
# And "e" can also be garbage collected, it's been destroyed.
|
||||
gc()
|
||||
expect_equal(ls(collected), c("c", "e"))
|
||||
|
||||
domain$end()
|
||||
# We can GC "a" as well; even though it references rv, it is
|
||||
# destroyed when the session ends.
|
||||
gc()
|
||||
expect_equal(sort(ls(collected)), c("a", "c", "e"))
|
||||
|
||||
# It's OK to turn on auto-destroy even after the session was
|
||||
# destroyed.
|
||||
d$setAutoDestroy(TRUE)
|
||||
# This should no-op.
|
||||
d$setAutoDestroy(FALSE)
|
||||
rm(d)
|
||||
gc()
|
||||
expect_equal(sort(ls(collected)), c("a", "c", "d", "e"))
|
||||
|
||||
rm(rv)
|
||||
# Both rv and "b" can now be collected.
|
||||
gc()
|
||||
expect_equal(sort(ls(collected)), c("a", "b", "c", "d", "e"))
|
||||
})
|
||||
|
||||
test_that("maskReactiveContext blocks use of reactives", {
|
||||
vals <- reactiveValues(x = 123)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user