Compare commits

...

104 Commits

Author SHA1 Message Date
Joe Cheng
50ac13d3fd [BREAKING] Modify API of downloadHandler
The `content` function now takes a file path, not writable connection, as an argument.
This makes it much easier to work with APIs that only write to file paths, not
connections.
2012-11-29 17:14:44 -08:00
Joe Cheng
58318fec46 Update package metadata for v0.2.0 2012-11-27 16:32:27 -08:00
Joe Cheng
a49941113e Require Shiny at app startup
Some of our examples omit library(shiny) from the top of ui.R and server.R,
which worked fine before but not with the namespace fix from yesterday.
Requiring shiny at startup fixes the problem.
2012-11-27 16:29:01 -08:00
Joe Cheng
595801cb99 Trivial style copy edits to example 10_download 2012-11-26 21:48:12 -08:00
Joe Cheng
0b469f09df Fix subtle name resolution bugs
See in particular:
http://stackoverflow.com/questions/13575353/how-does-the-shiny-r-package-deal-with-data-frames

Also reported at different times by Dirk Eddelbuettel and Jay Emerson.

The observed behavior is that S3/S4 method dispatch does not always seem to
work; the desired methods are not invoked despite appearing to be in the
search path.

The problem was that sourcing files with local=TRUE creates a new environment
based on the parent frame, which in our case is Shiny's package environment.
What we really want is to read from the global environment but write to a
throwaway environment. The correct way to do that is to make a new environment
with .GlobalEnv as the parent.
2012-11-26 21:45:28 -08:00
Joe Cheng
1e1f4e4a47 Update metadata for 0.1.14 2012-11-24 01:47:47 -08:00
Joe Cheng
c63e2ae7c8 Fix slider animation controls 2012-11-24 00:30:44 -08:00
Joe Cheng
d3d3fa990e Update version metadata 2012-11-23 23:47:27 -08:00
Joe Cheng
21980b7e71 Clean up PNG file when no longer needed 2012-11-23 22:44:37 -08:00
Joe Cheng
844ca0d387 I am stupid. 2012-11-21 23:02:40 -08:00
Joe Cheng
972ae35300 Update metadata for 0.1.12 2012-11-21 22:44:19 -08:00
Joe Cheng
57bfb8eb96 Bring untar operations in-house
Very simple tweak to R's untar2 code was all that was
required to fix the built-in untar's problems with
gists. Seemed best to just fork it and start using
the forked version directly, regardless of what is
installed on your machine.
2012-11-21 22:37:47 -08:00
Joe Cheng
ed6e6a9fb2 Squash another cygwin warning 2012-11-21 21:43:32 -08:00
Joe Cheng
ed402267b6 Fix runGist cygwin warning bug 2012-11-21 21:39:16 -08:00
Joe Cheng
6eec570828 Add CSS hooks for app-wide busy indicators 2012-11-21 00:04:16 -08:00
Joe Cheng
22fc1e3f0b Add param docs 2012-11-20 18:08:59 -08:00
Joe Cheng
ae9bd868f1 Implement arbitrary file downloads 2012-11-20 17:42:34 -08:00
Joe Cheng
a887012aca Update metadata for v0.1.11 2012-11-19 17:22:57 -08:00
Joe Cheng
bc73048ab9 Fix IE8 slice bug
IE8 doesn't like slice(0, undefined)--rather than interpreting it as slice(0),
it returns an array of length 0.
2012-11-19 17:19:51 -08:00
Joe Cheng
c89dd6c379 Fix issue #41: reactiveTable should allow print options too 2012-11-19 15:26:34 -08:00
Joe Cheng
9662debe5e Dynamic plot sizing 2012-11-19 15:26:02 -08:00
Joe Cheng
057262d917 Update metadata for v0.1.10 2012-11-19 13:11:07 -08:00
Joe Cheng
b6723a6219 Add per-session GET infrastructure. Allow IE8/9 to avoid data URIs. 2012-11-19 13:08:09 -08:00
Joe Cheng
068f3e0a43 Merge pull request #32 from edwindj/master
small bug: checkboxInputGroup sets html attribute "selected" in stead of "checked"
2012-11-15 23:30:28 -08:00
Joe Cheng
95635a8c47 Issue #37: headerPanel HTML argument shows up in title 2012-11-13 01:52:33 -08:00
Joe Cheng
3ec2071820 Address issue #35: Allow modification of untar args 2012-11-13 00:09:27 -08:00
Joe Cheng
1696db3044 Fix issue #36: reactiveUI does not always correctly render sliders
There is a deeper problem here, that reactiveUI output that renders stuff to the <head> will generally not work. We're not in a position to fix that yet and this problem has been reported twice, so we'll just fix this instance by making the slider dependencies built into the framework.
2012-11-11 18:32:34 -08:00
Joe Cheng
e1a1eab2b3 More MIME types 2012-11-10 15:18:29 -08:00
Edwin de Jonge
f7865f3358 changed html attribute of checkboxInputGroup from "selected" into "checked" 2012-11-08 23:09:08 +01:00
Joe Cheng
6d5f8ed5f3 Pointer to Shiny homepage 2012-11-08 03:29:23 -08:00
Joe Cheng
96a737379f Add linked example 2012-11-07 10:36:42 -08:00
Joe Cheng
d73feec013 Turns out GitHub doesn't like iframes 2012-11-07 10:28:47 -08:00
Joe Cheng
2ccead1da5 Add example to README 2012-11-07 10:28:06 -08:00
Joe Cheng
8885f2717e Update version 2012-11-06 13:53:53 -08:00
Joe Cheng
4448ffc777 Add methods for including text, HTML, and Markdown files in UI 2012-11-06 13:38:52 -08:00
Joe Cheng
022d10c598 Export and document observe function 2012-11-06 10:03:11 -08:00
Joe Cheng
8e6b7043bd Shut down timer callbacks before runApp returns 2012-11-06 09:36:49 -08:00
Joe Cheng
66eaaff598 More customizable error display 2012-11-02 09:49:17 -07:00
Joe Cheng
478c6c134f Much less flicker when updating plots 2012-11-02 09:48:36 -07:00
Joe Cheng
b5d333ba6c Rev downloader code 2012-10-31 15:36:52 -07:00
Joe Cheng
81723d55ac Change T and F to TRUE and FALSE
TRUE and FALSE are keywords whereas T and F are just predefined variables that can be reassigned
2012-10-31 11:35:41 -07:00
Joe Cheng
fb784ce962 Merge pull request #28 from rstudio/list-to-vec
Change lists to vectors in UI elements
2012-10-31 10:00:21 -07:00
Winston Chang
5a37380900 Capture stderr in download() 2012-10-30 16:19:14 -05:00
Joe Cheng
b6300f3a5c More robust setInternet2 workaround 2012-10-30 12:31:36 -07:00
Winston Chang
a3e8a2d623 Re-roxygenize 2012-10-30 10:49:55 -05:00
Winston Chang
7b3a4bdc39 Use vectors instead of lists in UI elements 2012-10-30 10:47:05 -05:00
Joe Cheng
cc0b5e5e0f Remove problematic link (fails R CMD check)
I first attempted to remove \code, but I couldn't figure out how to get the # in the URL to work right under both web-based help and PDF.
2012-10-29 11:56:59 -07:00
Joe Cheng
5c3f7d8f94 Update NEWS 2012-10-29 11:47:27 -07:00
Joe Cheng
8c3f8cd450 Add wellPanel and bootstrapPage functions 2012-10-29 11:45:58 -07:00
Joe Cheng
046582711a Update NEWS 2012-10-29 11:22:30 -07:00
Joe Cheng
15756ec92d Case insensitive probing for server.R, ui.R, et al 2012-10-29 11:19:23 -07:00
Joe Cheng
fc49abc9fb Fix issue #27: Warnings cause reactive functions to stop executing 2012-10-29 11:09:13 -07:00
Winston Chang
4a9ff27f3e Download gists in binary mode 2012-10-26 16:38:18 -05:00
Joe Cheng
790e6f370f Remove RCurl dependency 2012-10-26 14:07:07 -07:00
Joe Cheng
16ccc1321d Update docs 2012-10-26 10:46:42 -07:00
Joe Cheng
8648c94dd4 Update version to 0.1.8 2012-10-26 10:43:53 -07:00
Joe Cheng
dc4eb720ae Introduce input type hints
These allow the server to use custom deserialization code on a per-type basis.
2012-10-26 10:28:40 -07:00
Joe Cheng
0b891ad557 Run a GitHub gist 2012-10-25 20:41:52 -07:00
Joe Cheng
e96193ae28 Do .Random.seed restoring correctly 2012-10-24 23:19:13 -07:00
Joe Cheng
3ff9075959 Update NEWS 2012-10-24 21:11:56 -07:00
Joe Cheng
c03842056c Convert JSON to UTF-8
If reactivePrint or reactiveText return non-ASCII characters on
Windows, it causes invalid UTF-8 strings to be received by the
browser which closes the websocket connection.

I'm not sure this is the right place to do encoding, but it seems
to me like this approach is likely to work best for the most
users (especially those who just aren't thinking about encoding).
If you want to handle encoding in the reactives themselves (for
example), use `options(shiny.transcode.json=F)`.
2012-10-24 21:10:09 -07:00
Joe Cheng
6df226b21c Add repeatable utility function to stabilize RNGs 2012-10-24 16:12:08 -07:00
Joe Cheng
7dfa7d7426 Fix issue #26: Shiny.OutputBindings not correctly exported 2012-10-24 14:41:32 -07:00
Joe Cheng
b8b1a891cf Add custom message handler support, console logging
If the server sends a message with a "custom" field, that field's value will
be passed to a custom window.Shiny.oncustommessage function, if it is defined.

Also add support for messages like so:
{
  console: [
    'line one',
    'line two'
  ]
}

This will cause "line one" and "line two" to be printed at the browser console.
2012-10-04 17:45:20 -07:00
Joe Cheng
7df0e8b0f9 Update docs for 0.1.6 2012-09-25 03:08:31 -07:00
Joe Cheng
ff072ae9d9 bindAll should send initial values to server 2012-09-25 01:29:52 -07:00
Joe Cheng
f81ca39741 Add uiOutput. Tweak comments. 2012-09-25 00:33:00 -07:00
Joe Cheng
3db1f2a98c Don't animate showing/hiding of conditionalPanel 2012-09-21 19:51:24 -07:00
Joe Cheng
4865df9be1 Mark fileInput and reactiveUI as experimental. 2012-09-21 19:50:50 -07:00
Joe Cheng
0c16f2c334 Fix broken imports 2012-09-21 14:00:03 -07:00
Joe Cheng
d01149620f Fix issue #19: Checkboxes and radios can't be added dynamically 2012-09-19 11:48:28 -05:00
Joe Cheng
ab9401f390 Fix issue #20: DESCRIPTION file should use Imports instead of Depends 2012-09-19 11:47:12 -05:00
Joe Cheng
3223c17b74 Update websockets dependency version 2012-09-15 00:52:04 -07:00
Joe Cheng
404035bcf0 Bump version number 2012-09-14 19:16:03 -07:00
Joe Cheng
a0185bb0b4 Introduce shiny.http.response.filter option
Allows post-processing of HTTP responses
2012-09-14 13:15:58 -07:00
Joe Cheng
1a591cd9f1 conditionalPanel now triggers show/shown/hide/hidden event 2012-09-07 00:44:20 -07:00
Joe Cheng
e9b81b2033 [BREAKING] Simplify input binding callbacks
InputBinding.subscribe used to have to call callbacks with at least two arguments,
now there is only one optional argument (allowDeferred). The binding argument in
particular was problematic because it required "var self=this;".
2012-09-06 12:06:15 -07:00
Joe Cheng
cbfc1e8ed1 Add reactiveUI output type 2012-09-05 15:22:34 -07:00
Joe Cheng
cb63338805 Allow htmlOutput to contain inputs/outputs 2012-09-05 11:17:39 -07:00
Joe Cheng
bcdc82ccee Add conditionalPanel; JS API changes
- bindAll/unbindAll added
- bindInput/bindOutput/unbindInput/unbindOutput removed
2012-09-05 09:40:40 -07:00
Joe Cheng
76a4cf6c34 Update NEWS 2012-08-31 23:21:04 -07:00
Joe Cheng
872f23b0f0 Improvements for output binding/unbinding
- When bound, outputs receive cached error/value
- On binding, (potentially all) output plot sizes are resent
2012-08-31 23:12:20 -07:00
Joe Cheng
e61f7405fd Upload example app should accept text/plain files 2012-08-31 22:39:45 -07:00
Joe Cheng
0714871b56 Improve blob handling browser compatibility 2012-08-31 22:39:26 -07:00
Joe Cheng
8a89fb2a1a Expose and fix Shiny.unbindOutputs 2012-08-31 18:29:42 -07:00
Joe Cheng
036544e3ed Eagerly evaluate output name 2012-08-31 12:33:13 -07:00
Joe Cheng
7a6784d809 Add missing param to prototype method 2012-08-31 11:48:21 -07:00
Joe Cheng
ed9301705b Refactor JS to use more consistent OOP style
(function() { }).call(Foo.prototype) for extending prototypes manually, and
$.extend for extending objects manually or prototypes inheriting from each
other.
2012-08-31 10:00:20 -07:00
Joe Cheng
21f9694574 Add NEWS for file upload 2012-08-30 22:10:16 -07:00
Joe Cheng
3a0b11b89d Add file upload feature
This feature is currently pretty rough. It only works in the most modern
browsers (depends on HTML5 File API, including Blob.slice) and doesn't
show upload progress.
2012-08-30 22:07:00 -07:00
Joe Cheng
d5272e3e74 Update version 2012-08-30 12:27:05 -07:00
Joe Cheng
b5197869db Update NEWS 2012-08-30 12:18:46 -07:00
Joe Cheng
5f775db40a Enhancements to Shiny transport
- JS can now do remote procedure calls (with return value or exception), not just message passing
- RPC calls can include non-JSON-compatible binary data (not compatible with IE)
2012-08-30 12:16:12 -07:00
Joe Cheng
9b84b83627 Allow binding and unbinding of Shiny input/output 2012-08-30 12:04:17 -07:00
Joe Cheng
b0d9b5762a Don't use WebSocket constant, it's not on IE8 2012-08-24 11:28:46 -07:00
Joe Cheng
8d9fd402be Check inheritance properly 2012-08-23 18:07:09 -07:00
Joe Cheng
73a44a4f8e Packages can register their own URL namespace
Helpful for serving up custom stylesheets, CSS, images, etc.
2012-08-23 13:08:08 -07:00
Joe Cheng
a7dd62249e Add checkboxGroupInput control 2012-08-22 13:39:19 -07:00
Joe Cheng
42fac871fb Extensible websocket creation 2012-08-22 12:32:33 -07:00
Joe Cheng
2782bf6735 Execute sendPlotSize when anything is shown/hidden 2012-08-21 14:18:00 -07:00
Joe Cheng
f2a1ce4977 Update NEWS 2012-08-21 14:16:25 -07:00
Joe Cheng
c8969c4cc0 Upgrade to Bootstrap 2.1 2012-08-21 11:35:37 -07:00
Joe Cheng
cfefb4a07c Update NEWS 2012-08-20 17:16:23 -07:00
Joe Cheng
653509368b Let Bootstrap tabset send its selected tab as input 2012-08-20 17:01:41 -07:00
58 changed files with 5505 additions and 1674 deletions

View File

@@ -1,25 +1,42 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 0.1.3
Date: 2012-08-20
Version: 0.2.1
Date: 2012-11-29
Author: RStudio, Inc.
Maintainer: Joe Cheng <joe@rstudio.org>
Description: Shiny makes it incredibly easy to build interactive web
applications with R. Automatic "reactive" binding between inputs and
outputs and extensive pre-built widgets make it possible to build
Description: Shiny makes it incredibly easy to build interactive web
applications with R. Automatic "reactive" binding between inputs and
outputs and extensive pre-built widgets make it possible to build
beautiful, responsive, and powerful applications with minimal effort.
License: GPL-3
Depends: R (>= 2.14.1), methods, websockets (>= 1.1.4), caTools, RJSONIO, xtable
Imports: stats, tools, utils, datasets
Depends:
R (>= 2.14.1)
Imports:
stats,
tools,
utils,
datasets,
methods,
websockets (>= 1.1.6),
caTools,
RJSONIO,
xtable,
digest
Suggests:
markdown
URL: https://github.com/rstudio/shiny, http://rstudio.github.com/shiny/tutorial
BugReports: https://github.com/rstudio/shiny/issues
Collate:
'map.R'
'utils.R'
'tar.R'
'timer.R'
'tags.R'
'cache.R'
'react.R'
'reactives.R'
'fileupload.R'
'shiny.R'
'shinywrappers.R'
'shinyui.R'

View File

@@ -1,10 +1,18 @@
export(a)
export(addResourcePath)
export(animationOptions)
export(bootstrapPage)
export(br)
export(checkboxGroupInput)
export(checkboxInput)
export(code)
export(conditionalPanel)
export(div)
export(downloadButton)
export(downloadHandler)
export(downloadLink)
export(em)
export(fileInput)
export(h1)
export(h2)
export(h3)
@@ -16,9 +24,13 @@ export(helpText)
export(HTML)
export(htmlOutput)
export(img)
export(includeHTML)
export(includeMarkdown)
export(includeText)
export(invalidateLater)
export(mainPanel)
export(numericInput)
export(observe)
export(p)
export(pageWithSidebar)
export(plotOutput)
@@ -30,8 +42,11 @@ export(reactivePrint)
export(reactiveTable)
export(reactiveText)
export(reactiveTimer)
export(reactiveUI)
export(repeatable)
export(runApp)
export(runExample)
export(runGist)
export(selectInput)
export(shinyServer)
export(shinyUI)
@@ -46,15 +61,26 @@ export(tabPanel)
export(tabsetPanel)
export(tag)
export(tagAppendChild)
export(tagList)
export(tags)
export(textInput)
export(textOutput)
export(uiOutput)
export(verbatimTextOutput)
export(wellPanel)
import(caTools)
import(digest)
import(RJSONIO)
import(websockets)
import(xtable)
S3method(as.character,shiny.tag)
S3method(as.character,shiny.tag.list)
S3method(as.list,reactvaluesreader)
S3method(format,shiny.tag)
S3method(format,shiny.tag.list)
S3method(names,reactvaluesreader)
S3method(print,shiny.tag)
S3method(print,shiny.tag.list)
S3method(reactive,default)
S3method(reactive,"function")
S3method("$",reactvaluesreader)

129
NEWS
View File

@@ -1,8 +1,137 @@
shiny 0.2.1
--------------------------------------------------------------------------------
* [BREAKING] Modify API of `downloadHandler`: The `content` function now takes
a file path, not writable connection, as an argument. This makes it much
easier to work with APIs that only write to file paths, not connections.
shiny 0.2.0
--------------------------------------------------------------------------------
* Fix subtle name resolution bug--the usual symptom being S4 methods not being
invoked correctly when called from inside of ui.R or server.R
shiny 0.1.14
--------------------------------------------------------------------------------
* Fix slider animator, which broke in 0.1.10
shiny 0.1.13
--------------------------------------------------------------------------------
* Fix temp file leak in reactivePlot
shiny 0.1.12
--------------------------------------------------------------------------------
* Fix problems with runGist on Windows
* Add feature for on-the-fly file downloads (e.g. CSV data, PDFs)
* Add CSS hooks for app-wide busy indicators
shiny 0.1.11
--------------------------------------------------------------------------------
* Fix input binding with IE8 on Shiny Server
* Fix issue #41: reactiveTable should allow print options too
* Allow dynamic sizing of reactivePlot (i.e. using a function instead of a fixed
value)
shiny 0.1.10
--------------------------------------------------------------------------------
* Support more MIME types when serving out of www
* Fix issue #35: Allow modification of untar args
* headerPanel can take an explicit window title parameter
* checkboxInput uses correct attribute `checked` instead of `selected`
* Fix plot rendering with IE8 on Shiny Server
shiny 0.1.9
--------------------------------------------------------------------------------
* Much less flicker when updating plots
* More customizable error display
* Add `includeText`, `includeHTML`, and `includeMarkdown` functions for putting
text, HTML, and Markdown content from external files in the application's UI.
shiny 0.1.8
--------------------------------------------------------------------------------
* Add `runGist` function for conveniently running a Shiny app that is published
on gist.github.com.
* Fix issue #27: Warnings cause reactive functions to stop executing.
* The server.R and ui.R filenames are now case insensitive.
* Add `wellPanel` function for creating inset areas on the page.
* Add `bootstrapPage` function for creating new Twitter Bootstrap based
layouts from scratch.
shiny 0.1.7
--------------------------------------------------------------------------------
* Fix issue #26: Shiny.OutputBindings not correctly exported.
* Add `repeatable` function for making easily repeatable versions of random
number generating functions.
* Transcode JSON into UTF-8 (prevents non-ASCII reactivePrint values from
causing errors on Windows).
shiny 0.1.6
--------------------------------------------------------------------------------
* Import package dependencies, instead of attaching them (with the exception of
websockets, which doesn't currently work unless attached).
* conditionalPanel was animated, now it is not.
* bindAll was not correctly sending initial values to the server; fixed.
shiny 0.1.5
--------------------------------------------------------------------------------
* BREAKING CHANGE: JS APIs Shiny.bindInput and Shiny.bindOutput removed and
replaced with Shiny.bindAll; Shiny.unbindInput and Shiny.unbindOutput removed
and replaced with Shiny.unbindAll.
* Add file upload support (currently only works with Chrome and Firefox). Use
a normal HTML file input, or call the `fileInput` UI function.
* Shiny.unbindOutputs did not work, now it does.
* Generally improved robustness of dynamic input/output bindings.
* Add conditionalPanel UI function to allow showing/hiding UI based on a JS
expression; for example, whether an input is a particular value. Also works in
raw HTML (add the `data-display-if` attribute to the element that should be
shown/hidden).
* htmlOutput (CSS class `shiny-html-output`) can contain inputs and outputs.
shiny 0.1.4
--------------------------------------------------------------------------------
* Allow Bootstrap tabsets to act as reactive inputs; their value indicates which
tab is active
* Upgrade to Bootstrap 2.1
* Add `checkboxGroupInput` control, which presents a list of checkboxes and
returns a vector of the selected values
* Add `addResourcePath`, intended for reusable component authors to access CSS,
JavaScript, image files, etc. from their package directories
* Add Shiny.bindInputs(scope), .unbindInputs(scope), .bindOutputs(scope), and
.unbindOutputs(scope) JS API calls to allow dynamic binding/unbinding of HTML
elements
shiny 0.1.3
--------------------------------------------------------------------------------
* Introduce Shiny.inputBindings.register JS API and InputBinding class, for
creating custom input controls
* Add `step` parameter to numericInput
* Read names of input using `names(input)`
* Access snapshot of input as a list using `as.list(input)`
* Fix issue #10: Plots in tabsets not rendered

View File

@@ -1,3 +1,63 @@
#' Create a Twitter Bootstrap page
#'
#' Create a Shiny UI page that loads the CSS and JavaScript for
#' \href{http://getbootstrap.com}{Twitter Bootstrap}, and has no content in the
#' page body (other than what you provide).
#'
#' This function is primarily intended for users who are proficient in HTML/CSS,
#' and know how to lay out pages in Bootstrap. Most users should use template
#' functions like \code{\link{pageWithSidebar}}.
#'
#' @param ... The contents of the document body.
#' @return A UI defintion that can be passed to the \link{shinyUI} function.
#'
#' @export
bootstrapPage <- function(...) {
# required head tags for boostrap
importBootstrap <- function(min = TRUE, responsive = TRUE) {
ext <- function(ext) {
ifelse(min, paste(".min", ext, sep=""), ext)
}
cssExt <- ext(".css")
jsExt = ext(".js")
bs <- "shared/bootstrap/"
result <- tags$head(
tags$link(rel="stylesheet",
type="text/css",
href="shared/slider/css/jquery.slider.min.css"),
tags$script(src="shared/slider/js/jquery.slider.min.js"),
tags$link(rel="stylesheet",
type="text/css",
href=paste(bs, "css/bootstrap", cssExt, sep="")),
tags$script(src=paste(bs, "js/bootstrap", jsExt, sep=""))
)
if (responsive) {
result <- tagAppendChild(
result,
tags$meta(name="viewport",
content="width=device-width, initial-scale=1.0"))
result <- tagAppendChild(
result,
tags$link(rel="stylesheet",
type="text/css",
href=paste(bs, "css/bootstrap-responsive", cssExt, sep="")))
}
result
}
tagList(
# inject bootstrap requirements into head
importBootstrap(),
list(...)
)
}
#' Create a page with a sidebar
#'
@@ -34,43 +94,16 @@
#' @export
pageWithSidebar <- function(headerPanel, sidebarPanel, mainPanel) {
# required head tags for boostrap
importBootstrap <- function(min = TRUE) {
ext <- function(ext) {
ifelse(min, paste(".min", ext, sep=""), ext)
}
cssExt <- ext(".css")
jsExt = ext(".js")
bs <- "shared/bootstrap/"
tags$head(
tags$meta(name="viewport",
content="width=device-width, initial-scale=1.0"),
tags$link(rel="stylesheet",
type="text/css",
href=paste(bs, "css/bootstrap", cssExt, sep="")),
tags$link(rel="stylesheet",
type="text/css",
href=paste(bs, "css/bootstrap-responsive", cssExt, sep="")),
tags$script(src=paste(bs, "js/bootstrap", jsExt, sep=""))
)
}
list(
# inject bootstrap requirements into head
importBootstrap(),
bootstrapPage(
# basic application container divs
div(class="container-fluid",
div(
class="container-fluid",
div(class="row-fluid",
headerPanel
headerPanel
),
div(class="row-fluid",
sidebarPanel,
mainPanel
sidebarPanel,
mainPanel
)
)
)
@@ -82,20 +115,35 @@ pageWithSidebar <- function(headerPanel, sidebarPanel, mainPanel) {
#' Create a header panel containing an application title.
#'
#' @param title An application title to display
#' @param windowTitle The title that should be displayed by the browser window.
#' Useful if \code{title} is not a string.
#' @return A headerPanel that can be passed to \link{pageWithSidebar}
#'
#'
#' @examples
#' headerPanel("Hello Shiny!")
#' @export
headerPanel <- function(title) {
list(
tags$head(tags$title(title)),
headerPanel <- function(title, windowTitle=title) {
tagList(
tags$head(tags$title(windowTitle)),
div(class="span12", style="padding: 10px 0px;",
h1(title)
)
)
}
#' Create a well panel
#'
#' Creates a panel with a slightly inset border and grey background. Equivalent
#' to Twitter Bootstrap's \code{well} CSS class.
#'
#' @param ... UI elements to include inside the panel.
#' @return The newly created panel.
#'
#' @export
wellPanel <- function(...) {
div(class="well", ...)
}
#' Create a sidebar panel
#'
#' Create a sidebar panel containing input controls that can in turn be
@@ -143,6 +191,52 @@ mainPanel <- function(...) {
)
}
#' Conditional Panel
#'
#' Creates a panel that is visible or not, depending on the value of a
#' JavaScript expression. The JS expression is evaluated once at startup and
#' whenever Shiny detects a relevant change in input/output.
#'
#' In the JS expression, you can refer to \code{input} and \code{output}
#' JavaScript objects that contain the current values of input and output. For
#' example, if you have an input with an id of \code{foo}, then you can use
#' \code{input.foo} to read its value. (Be sure not to modify the input/output
#' objects, as this may cause unpredictable behavior.)
#'
#' @param condition A JavaScript expression that will be evaluated repeatedly to
#' determine whether the panel should be displayed.
#' @param ... Elements to include in the panel.
#'
#' @examples
#' sidebarPanel(
#' selectInput(
#' "plotType", "Plot Type",
#' c(Scatter = "scatter",
#' Histogram = "hist")),
#'
#' # Only show this panel if the plot type is a histogram
#' conditionalPanel(
#' condition = "input.plotType == 'hist'",
#' selectInput(
#' "breaks", "Breaks",
#' c("Sturges",
#' "Scott",
#' "Freedman-Diaconis",
#' "[Custom]" = "custom")),
#'
#' # Only show this panel if Custom is selected
#' conditionalPanel(
#' condition = "input.breaks == 'custom'",
#' sliderInput("breakCount", "Break Count", min=1, max=1000, value=10)
#' )
#' )
#' )
#'
#' @export
conditionalPanel <- function(condition, ...) {
div('data-display-if'=condition, ...)
}
#' Create a text input control
#'
#' Create an input control for entry of unstructured text values
@@ -156,7 +250,7 @@ mainPanel <- function(...) {
#' textInput("caption", "Caption:", "Data Summary")
#' @export
textInput <- function(inputId, label, value = "") {
list(
tagList(
tags$label(label),
tags$input(id = inputId, type="text", value=value)
)
@@ -189,51 +283,125 @@ numericInput <- function(inputId, label, value, min = NA, max = NA, step = NA) {
if (!is.na(step))
inputTag$attribs$step = step
list(
tagList(
tags$label(label),
inputTag
)
}
#' Create a checkbox input control
#' File Upload Control
#'
#' Create a checkbox that can be used to specify logical values
#' Create a file upload control that can be used to upload one or more files.
#' \bold{Experimental feature. Only works in some browsers (primarily tested on
#' Chrome and Firefox).}
#'
#' @param inputId Input variable to assign the control's value to
#' @param label Display label for the control
#' @param value Initial value
#' @param inputId Input variable to assign the control's value to.
#' @param label Display label for the control.
#' @param multiple Whether the user should be allowed to select and upload
#' multiple files at once.
#' @param accept A character vector of MIME types; gives the browser a hint of
#' what kind of files the server is expecting.
#'
#' @export
fileInput <- function(inputId, label, multiple = FALSE, accept = NULL) {
inputTag <- tags$input(id = inputId, type = "file")
if (multiple)
inputTag$attribs$multiple <- "multiple"
if (length(accept) > 0)
inputTag$attribs$accept <- paste(accept, collapse=',')
tagList(
tags$label(label),
inputTag
)
}
#' Checkbox Input Control
#'
#' Create a checkbox that can be used to specify logical values.
#'
#' @param inputId Input variable to assign the control's value to.
#' @param label Display label for the control.
#' @param value Initial value (\code{TRUE} or \code{FALSE}).
#' @return A checkbox control that can be added to a UI definition.
#'
#' @seealso \code{\link{checkboxGroupInput}}
#'
#' @examples
#' checkboxInput("outliers", "Show outliers", FALSE)
#' @export
checkboxInput <- function(inputId, label, value = FALSE) {
inputTag <- tags$input(id = inputId, type="checkbox")
if (value)
if (!is.null(value) && value)
inputTag$attribs$checked <- "checked"
tags$label(class = "checkbox", inputTag, label)
}
#' Checkbox Group Input Control
#'
#' Create a group of checkboxes that can be used to toggle multiple choices
#' independently. The server will receive the input as a character vector of the
#' selected values.
#'
#' @param inputId Input variable to assign the control's value to.
#' @param label Display label for the control.
#' @param choices List of values to show checkboxes for. If elements of the list
#' are named then that name rather than the value is displayed to the user.
#' @param selected Names of items that should be initially selected, if any.
#' @return A list of HTML elements that can be added to a UI definition.
#'
#' @seealso \code{\link{checkboxInput}}
#'
#' @examples
#' checkboxGroupInput("variable", "Variable:",
#' c("Cylinders" = "cyl",
#' "Transmission" = "am",
#' "Gears" = "gear"))
#'
#' @export
checkboxGroupInput <- function(inputId, label, choices, selected = NULL) {
# resolve names
choices <- choicesWithNames(choices)
checkboxes <- list()
for (choiceName in names(choices)) {
checkbox <- tags$input(name = inputId, type="checkbox",
value = choices[[choiceName]])
if (choiceName %in% selected)
checkbox$attribs$checked <- 'checked'
checkboxes[[length(checkboxes)+1]] <- checkbox
checkboxes[[length(checkboxes)+1]] <- choiceName
checkboxes[[length(checkboxes)+1]] <- tags$br()
}
# return label and select tag
tags$div(class='control-group',
controlLabel(inputId, label),
checkboxes)
}
#' Create a help text element
#'
#' Create help text which can be added to an input form to provide
#' additional explanation or context.
#' Create help text which can be added to an input form to provide additional
#' explanation or context.
#'
#' @param text Help text string
#' @param ... Additional help text strings
#' @param ... One or more help text strings (or other inline HTML elements)
#' @return A help text element that can be added to a UI definition.
#'
#'
#' @examples
#' helpText("Note: while the data view will show only",
#' "the specified number of observations, the",
#' "summary will be based on the full dataset.")
#' @export
helpText <- function(text, ...) {
text <- c(text, as.character(list(...)))
text <- paste(text, collapse=" ")
span(class="help-block", text)
helpText <- function(...) {
span(class="help-block", ...)
}
controlLabel <- function(controlName, label) {
@@ -272,9 +440,9 @@ choicesWithNames <- function(choices) {
#'
#' @examples
#' selectInput("variable", "Variable:",
#' list("Cylinders" = "cyl",
#' "Transmission" = "am",
#' "Gears" = "gear"))
#' c("Cylinders" = "cyl",
#' "Transmission" = "am",
#' "Gears" = "gear"))
#' @export
selectInput <- function(inputId,
label,
@@ -300,7 +468,7 @@ selectInput <- function(inputId,
}
# return label and select tag
list(controlLabel(inputId, label), selectTag)
tagList(controlLabel(inputId, label), selectTag)
}
#' Create radio buttons
@@ -317,10 +485,10 @@ selectInput <- function(inputId,
#'
#' @examples
#' radioButtons("dist", "Distribution type:",
#' list("Normal" = "norm",
#' "Uniform" = "unif",
#' "Log-normal" = "lnorm",
#' "Exponential" = "exp"))
#' c("Normal" = "norm",
#' "Uniform" = "unif",
#' "Log-normal" = "lnorm",
#' "Exponential" = "exp"))
#' @export
radioButtons <- function(inputId, label, choices, selected = NULL) {
# resolve names
@@ -349,8 +517,8 @@ radioButtons <- function(inputId, label, choices, selected = NULL) {
inputTags[[length(inputTags) + 1]] <- labelTag
}
list(tags$label(class = "control-label", label),
inputTags)
tagList(tags$label(class = "control-label", label),
inputTags)
}
#' Create a submit button
@@ -421,10 +589,10 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
if (!is.character(labelText))
stop("label not specified")
if (identical(animate, T))
if (identical(animate, TRUE))
animate <- animationOptions()
if (!is.null(animate) && !identical(animate, F)) {
if (!is.null(animate) && !identical(animate, FALSE)) {
if (is.null(animate$playButton))
animate$playButton <- tags$i(class='icon-play')
if (is.null(animate$pauseButton))
@@ -432,7 +600,7 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
}
# build slider
list(
tagList(
controlLabel(inputId, labelText),
slider(inputId, min=min, max=max, value=value, step=step, round=round,
locale=locale, format=format, ticks=ticks,
@@ -443,12 +611,15 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
#' Create a tab panel
#'
#' Create a tab panel that can be inluded within a \link{tabsetPanel}.
#' Create a tab panel that can be included within a \code{\link{tabsetPanel}}.
#'
#' @param title Display title for tab
#' @param ... UI elements to include within the tab
#' @return A tab that can be passed to \link{tabsetPanel}
#'
#' @param value The value that should be sent when \code{tabsetPanel} reports
#' that this tab is selected. If omitted and \code{tabsetPanel} has an
#' \code{id}, then the title will be used.
#' @return A tab that can be passed to \code{\link{tabsetPanel}}
#'
#' @examples
#' # Show a tabset that includes a plot, summary, and
#' # table view of the generated distribution
@@ -460,19 +631,22 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
#' )
#' )
#' @export
tabPanel <- function(title, ...) {
div(class="tab-pane", title=title, ...)
tabPanel <- function(title, ..., value = NULL) {
div(class="tab-pane", title=title, `data-value`=value, ...)
}
#' Create a tabset panel
#'
#' Create a tabset that contains \link{tabPanel} elements. Tabsets
#' are useful for dividing output into multiple independently viewable
#' sections.
#'
#' @param ... \link{tabPanel} elements to include in the tabset
#' @return A tabset that can be passed to \link{mainPanel}
#' Create a tabset that contains \code{\link{tabPanel}} elements. Tabsets are
#' useful for dividing output into multiple independently viewable sections.
#'
#' @param ... \code{\link{tabPanel}} elements to include in the tabset
#' @param id If provided, you can use \code{input$}\emph{\code{id}} in your server
#' logic to determine which of the current tabs is active. The value will
#' correspond to the \code{value} argument that is passed to
#' \code{\link{tabPanel}}.
#' @return A tabset that can be passed to \code{\link{mainPanel}}
#'
#' @examples
#' # Show a tabset that includes a plot, summary, and
#' # table view of the generated distribution
@@ -484,24 +658,31 @@ tabPanel <- function(title, ...) {
#' )
#' )
#' @export
tabsetPanel <- function(...) {
tabsetPanel <- function(..., id = NULL) {
# build tab-nav and tab-content divs
tabs <- list(...)
tabNavList <- tags$ul(class = "nav nav-tabs")
tabNavList <- tags$ul(class = "nav nav-tabs", id = id)
tabContent <- tags$div(class = "tab-content")
firstTab <- TRUE
tabsetId <- as.integer(stats::runif(1, 1, 10000))
tabId <- 1
for (divTag in tabs) {
# compute id and assign it to the div
id <- paste("tab", tabsetId, tabId, sep="-")
divTag$attribs$id <- id
thisId <- paste("tab", tabsetId, tabId, sep="-")
divTag$attribs$id <- thisId
tabId <- tabId + 1
tabValue <- divTag$attribs$`data-value`
if (!is.null(tabValue) && is.null(id)) {
stop("tabsetPanel doesn't have an id assigned, but one of its tabPanels ",
"has a value. The value won't be sent without an id.")
}
# create the li tag
liTag <- tags$li(tags$a(href=paste("#", id, sep=""),
liTag <- tags$li(tags$a(href=paste("#", thisId, sep=""),
`data-toggle` = "tab",
`data-value` = tabValue,
divTag$attribs$title))
# set the first tab as active
@@ -594,6 +775,10 @@ tableOutput <- function(outputId) {
#' Render a reactive output variable as HTML within an application page. The
#' text will be included within an HTML \code{div} tag, and is presumed to
#' contain HTML content which should not be escaped.
#'
#' \code{uiOutput} is intended to be used with \code{reactiveUI} on the
#' server side. It is currently just an alias for \code{htmlOutput}.
#'
#' @param outputId output variable to read the value from
#' @return An HTML output element that can be included in a panel
#' @examples
@@ -602,3 +787,58 @@ tableOutput <- function(outputId) {
htmlOutput <- function(outputId) {
div(id = outputId, class="shiny-html-output")
}
#' @rdname htmlOutput
#' @export
uiOutput <- function(outputId) {
htmlOutput(outputId)
}
#' Create a download button or link
#'
#' Use these functions to create a download button or link; when clicked, it
#' will initiate a browser download. The filename and contents are specified by
#' the corresponding \code{\link{downloadHandler}} defined in the server
#' function.
#'
#' @param outputId The name of the output slot that the \code{downloadHandler}
#' is assigned to.
#' @param label The label that should appear on the button.
#' @param class Additional CSS classes to apply to the tag, if any.
#'
#' @examples
#' \dontrun{
#' # In server.R:
#' output$downloadData <- downloadHandler(
#' filename = function() {
#' paste('data-', Sys.Date(), '.csv', sep='')
#' },
#' content = function(con) {
#' write.csv(data, con)
#' }
#' )
#'
#' # In ui.R:
#' downloadLink('downloadData', 'Download')
#' }
#'
#' @aliases downloadLink
#' @seealso downloadHandler
#' @export
downloadButton <- function(outputId, label="Download", class=NULL) {
tags$a(id=outputId,
class=paste(c('btn shiny-download-link', class), collapse=" "),
href='',
target='_blank',
label)
}
#' @rdname downloadButton
#' @export
downloadLink <- function(outputId, label="Download", class=NULL) {
tags$a(id=outputId,
class=paste(c('shiny-download-link', class), collapse=" "),
href='',
target='_blank',
label)
}

80
R/cache.R Normal file
View File

@@ -0,0 +1,80 @@
# A context object for tracking a cache that needs to be dirtied when a set of
# files changes on disk. Each time the cache is dirtied, the set of files is
# cleared. Therefore, the set of files needs to be re-built each time the cached
# code executes. This approach allows for dynamic dependency graphs.
CacheContext <- setRefClass(
'CacheContext',
fields = list(
.dirty = 'logical',
.tests = 'list'
),
methods = list(
initialize = function() {
.dirty <<- TRUE
# List of functions that return TRUE if dirty
.tests <<- list()
},
addDependencyFile = function(file) {
if (.dirty)
return()
file <- normalizePath(file)
mtime <- file.info(file)$mtime
.tests <<- c(.tests, function() {
newMtime <- try(file.info(file)$mtime, silent=TRUE)
if (is(newMtime, 'try-error'))
return(TRUE)
return(!identical(mtime, newMtime))
})
invisible()
},
forceDirty = function() {
.dirty <<- TRUE
.tests <<- list()
invisible()
},
isDirty = function() {
if (.dirty)
return(TRUE)
for (test in .tests) {
if (test()) {
forceDirty()
return(TRUE)
}
}
return(FALSE)
},
reset = function() {
.dirty <<- FALSE
.tests <<- list()
},
with = function(func) {
oldCC <- .currentCacheContext$cc
.currentCacheContext$cc <- .self
on.exit(.currentCacheContext$cc <- oldCC)
return(func())
}
)
)
.currentCacheContext <- new.env()
# Indicates to Shiny that the given file path is part of the dependency graph
# for whatever is currently executing (so far, only ui.R). By default, ui.R only
# gets re-executed when it is detected to have changed; this function allows the
# caller to indicate that it should also re-execute if the given file changes.
#
# If NULL or NA is given as the argument, then ui.R will re-execute next time.
dependsOnFile <- function(filepath) {
if (is.null(.currentCacheContext$cc))
stop("addFileDependency was called at an unexpected time (no cache context found)")
if (is.null(filepath) || is.na(filepath))
.currentCacheContext$cc$forceDirty()
else
.currentCacheContext$cc$addDependencyFile(filepath)
}

95
R/fileupload.R Normal file
View File

@@ -0,0 +1,95 @@
# For HTML5-capable browsers, file uploads happen through a series of requests.
#
# 1. Client tells server that one or more files are about to be uploaded; the
# server responds with a "job ID" that the client should use for the rest of
# the upload.
#
# 2. For each file (sequentially):
# a. Client tells server the name, size, and type of the file.
# b. Client sends server a small-ish blob of data.
# c. Repeat 2b until the entire file has been uploaded.
# d. Client tells server that the current file is done.
#
# 3. Repeat 2 until all files have been uploaded.
#
# 4. Client tells server that all files have been uploaded, along with the
# input ID that this data should be associated with.
#
# Unfortunately this approach will not work for browsers that don't support
# HTML5 File API, but the fallback approach we would like to use (multipart
# form upload, i.e. traditional HTTP POST-based file upload) doesn't work with
# the websockets package's HTTP server at the moment.
FileUploadOperation <- setRefClass(
'FileUploadOperation',
fields = list(
.parent = 'ANY',
.id = 'character',
.files = 'data.frame',
.dir = 'character',
.currentFileInfo = 'list',
.currentFileData = 'ANY'
),
methods = list(
initialize = function(parent, id, dir) {
.parent <<- parent
.id <<- id
.dir <<- dir
},
fileBegin = function(file) {
.currentFileInfo <<- file
filename <- file.path(.dir, as.character(length(.files)))
row <- data.frame(name=file$name, size=file$size, type=file$type,
datapath=filename, stringsAsFactors=FALSE)
if (length(.files) == 0)
.files <<- row
else
.files <<- rbind(.files, row)
.currentFileData <<- file(filename, open='wb')
},
fileChunk = function(rawdata) {
writeBin(rawdata, .currentFileData)
},
fileEnd = function() {
close(.currentFileData)
},
finish = function() {
.parent$onJobFinished(.id)
return(.files)
}
)
)
FileUploadContext <- setRefClass(
'FileUploadContext',
fields = list(
.basedir = 'character',
.operations = 'Map'
),
methods = list(
initialize = function(dir=tempdir()) {
.basedir <<- dir
},
createUploadOperation = function() {
while (TRUE) {
id <- paste(as.raw(runif(12, min=0, max=0xFF)), collapse='')
dir <- file.path(.basedir, id)
if (!dir.create(dir))
next
op <- FileUploadOperation$new(.self, id, dir)
.operations$set(id, op)
return(id)
}
},
getUploadOperation = function(jobId) {
.operations$get(jobId)
},
onJobFinished = function(jobId) {
.operations$remove(jobId)
}
)
)

20
R/map.R
View File

@@ -20,30 +20,36 @@ Map <- setRefClass(
},
get = function(key) {
if (.self$containsKey(key))
return(base::get(key, pos=.env, inherits=F))
return(base::get(key, pos=.env, inherits=FALSE))
else
return(NULL)
},
set = function(key, value) {
assign(key, value, pos=.env, inherits=F)
assign(key, value, pos=.env, inherits=FALSE)
return(value)
},
mset = function(...) {
args <- list(...)
for (key in names(args))
set(key, args[[key]])
return()
},
remove = function(key) {
if (.self$containsKey(key)) {
result <- .self$get(key)
rm(list = key, pos=.env, inherits=F)
rm(list = key, pos=.env, inherits=FALSE)
return(result)
}
return(NULL)
},
containsKey = function(key) {
exists(key, where=.env, inherits=F)
exists(key, where=.env, inherits=FALSE)
},
keys = function() {
ls(envir=.env, all.names=T)
ls(envir=.env, all.names=TRUE)
},
values = function() {
mget(.self$keys(), envir=.env, inherits=F)
mget(.self$keys(), envir=.env, inherits=FALSE)
},
clear = function() {
.env <<- new.env(parent=emptyenv())
@@ -67,7 +73,7 @@ Map <- setRefClass(
as.list.Map <- function(map) {
sapply(map$keys(),
map$get,
simplify=F)
simplify=FALSE)
}
length.Map <- function(map) {
map$size()

View File

@@ -9,7 +9,7 @@ Context <- setRefClass(
methods = list(
initialize = function() {
id <<- .getReactiveEnvironment()$nextId()
.invalidated <<- F
.invalidated <<- FALSE
.callbacks <<- list()
.hintCallbacks <<- list()
},
@@ -32,7 +32,7 @@ Context <- setRefClass(
invalidated until the next call to \\code{\\link{flushReact}}."
if (.invalidated)
return()
.invalidated <<- T
.invalidated <<- TRUE
.getReactiveEnvironment()$addPendingInvalidate(.self)
NULL
},
@@ -52,14 +52,12 @@ Context <- setRefClass(
executeCallbacks = function() {
"For internal use only."
lapply(.callbacks, function(func) {
tryCatch({
withCallingHandlers({
func()
}, warning = function(e) {
# TODO: Callbacks in app
print(e)
}, error = function(e) {
# TODO: Callbacks in app
print(e)
})
})
}
@@ -109,10 +107,10 @@ ReactiveEnvironment <- setRefClass(
)
.getReactiveEnvironment <- function() {
if (!exists('.ReactiveEnvironment', envir=.GlobalEnv, inherits=F)) {
if (!exists('.ReactiveEnvironment', envir=.GlobalEnv, inherits=FALSE)) {
assign('.ReactiveEnvironment', ReactiveEnvironment$new(), envir=.GlobalEnv)
}
get('.ReactiveEnvironment', envir=.GlobalEnv, inherits=F)
get('.ReactiveEnvironment', envir=.GlobalEnv, inherits=FALSE)
}
# Causes any pending invalidations to run.

View File

@@ -52,21 +52,21 @@ Values <- setRefClass(
get = function(key) {
ctx <- .getReactiveEnvironment()$currentContext()
dep.key <- paste(key, ':', ctx$id, sep='')
if (!exists(dep.key, where=.dependencies, inherits=F)) {
assign(dep.key, ctx, pos=.dependencies, inherits=F)
if (!exists(dep.key, where=.dependencies, inherits=FALSE)) {
assign(dep.key, ctx, pos=.dependencies, inherits=FALSE)
ctx$onInvalidate(function() {
rm(list=dep.key, pos=.dependencies, inherits=F)
rm(list=dep.key, pos=.dependencies, inherits=FALSE)
})
}
if (!exists(key, where=.values, inherits=F))
if (!exists(key, where=.values, inherits=FALSE))
NULL
else
base::get(key, pos=.values, inherits=F)
base::get(key, pos=.values, inherits=FALSE)
},
set = function(key, value) {
if (exists(key, where=.values, inherits=F)) {
if (identical(base::get(key, pos=.values, inherits=F), value)) {
if (exists(key, where=.values, inherits=FALSE)) {
if (identical(base::get(key, pos=.values, inherits=FALSE), value)) {
return(invisible())
}
}
@@ -75,11 +75,11 @@ Values <- setRefClass(
}
.allDeps$invalidate()
assign(key, value, pos=.values, inherits=F)
assign(key, value, pos=.values, inherits=FALSE)
dep.keys <- objects(
pos=.dependencies,
pattern=paste('^\\Q', key, ':', '\\E', '\\d+$', sep=''),
all.names=T
all.names=TRUE
)
lapply(
mget(dep.keys, envir=.dependencies),
@@ -99,7 +99,7 @@ Values <- setRefClass(
},
names = function() {
.namesDeps$register()
return(ls(.values, all.names=T))
return(ls(.values, all.names=TRUE))
},
toList = function() {
.allDeps$register()
@@ -153,11 +153,11 @@ Observable <- setRefClass(
"or more parameters; only functions without parameters can be ",
"reactive.")
.func <<- func
.initialized <<- F
.initialized <<- FALSE
},
getValue = function() {
if (!.initialized) {
.initialized <<- T
.initialized <<- TRUE
.self$.updateValue()
}
@@ -178,7 +178,7 @@ Observable <- setRefClass(
.dependencies$invalidateHint()
})
ctx$run(function() {
.value <<- try(.func(), silent=F)
.value <<- try(.func(), silent=FALSE)
})
if (!identical(old.value, .value)) {
.dependencies$invalidate()
@@ -261,21 +261,29 @@ Observer <- setRefClass(
)
)
# NOTE: we de-roxygenized this comment because the function isn't exported
# Observe
#
# Creates an observer from the given function. An observer is like a reactive
# function in that it can read reactive values and call reactive functions,
# and will automatically re-execute when those dependencies change. But unlike
# reactive functions, it doesn't yield a result and can't be used as an input
# to other reactive functions. Thus, observers are only useful for their side
# effects (for example, performing I/O).
#
# @param func The function to observe. It must not have any parameters. Any
# return value from this function will be ignored.
#
#' Create a reactive observer
#'
#' Creates an observer from the given function. An observer is like a reactive
#' function in that it can read reactive values and call reactive functions, and
#' will automatically re-execute when those dependencies change. But unlike
#' reactive functions, it doesn't yield a result and can't be used as an input
#' to other reactive functions. Thus, observers are only useful for their side
#' effects (for example, performing I/O).
#'
#' Another contrast between reactive functions and observers is their execution
#' strategy. Reactive functions use lazy evaluation; that is, when their
#' dependencies change, they don't re-execute right away but rather wait until
#' they are called by someone else. Indeed, if they are not called then they
#' will never re-execute. In contrast, observers use eager evaluation; as soon
#' as their dependencies change, they schedule themselves to re-execute.
#'
#' @param func The function to observe. It must not have any parameters. Any
#' return value from this function will be ignored.
#'
#' @export
observe <- function(func) {
Observer$new(func)
invisible()
}
#' Timer

590
R/shiny.R
View File

@@ -1,8 +1,17 @@
#' @docType package
#' @import websockets caTools RJSONIO xtable digest
NULL
suppressPackageStartupMessages({
library(websockets)
library(RJSONIO)
})
createUniqueId <- function(bytes) {
# TODO: Use a method that isn't affected by the R seed
paste(as.character(as.raw(floor(runif(bytes, min=1, max=255)))), collapse='')
}
ShinyApp <- setRefClass(
'ShinyApp',
fields = list(
@@ -10,7 +19,12 @@ ShinyApp <- setRefClass(
.invalidatedOutputValues = 'Map',
.invalidatedOutputErrors = 'Map',
.progressKeys = 'character',
session = 'Values'
.fileUploadContext = 'FileUploadContext',
session = 'Values',
token = 'character', # Used to identify this instance in URLs
plots = 'Map',
downloads = 'Map',
allowDataUriScheme = 'logical'
),
methods = list(
initialize = function(ws) {
@@ -18,13 +32,24 @@ ShinyApp <- setRefClass(
.invalidatedOutputValues <<- Map$new()
.invalidatedOutputErrors <<- Map$new()
.progressKeys <<- character(0)
# TODO: Put file upload context in user/app-specific dir if possible
.fileUploadContext <<- FileUploadContext$new()
session <<- Values$new()
token <<- createUniqueId(16)
allowDataUriScheme <<- TRUE
},
defineOutput = function(name, func) {
"Binds an output generating function to this name. The function can either
take no parameters, or have named parameters for \\code{name} and
\\code{shinyapp} (in the future this list may expand, so it is a good idea
to also include \\code{...} in your function signature)."
# jcheng 08/31/2012: User submitted an example of a dynamically calculated
# name not working unless name was eagerly evaluated. Yikes!
force(name)
if (is.function(func)) {
if (length(formals(func)) != 0) {
orig <- func
@@ -35,12 +60,12 @@ ShinyApp <- setRefClass(
obs <- Observer$new(function() {
value <- try(func(), silent=F)
value <- try(func(), silent=FALSE)
.invalidatedOutputErrors$remove(name)
.invalidatedOutputValues$remove(name)
if (identical(class(value), 'try-error')) {
if (inherits(value, 'try-error')) {
cond <- attr(value, 'condition')
.invalidatedOutputErrors$set(
name,
@@ -76,10 +101,7 @@ ShinyApp <- setRefClass(
json <- toJSON(list(errors=as.list(errors),
values=as.list(values)))
if (getOption('shiny.trace', F))
message("SEND ", json)
websocket_write(json, .websocket)
.write(json)
},
showProgress = function(id) {
'Send a message to the client that recalculation of the output identified
@@ -93,10 +115,173 @@ ShinyApp <- setRefClass(
json <- toJSON(list(progress=list(id)))
if (getOption('shiny.trace', F))
message("SEND ", json)
.write(json)
},
dispatch = function(msg) {
method <- paste('@', msg$method, sep='')
func <- try(do.call(`$`, list(.self, method)), silent=TRUE)
if (inherits(func, 'try-error')) {
.sendErrorResponse(msg, paste('Unknown method', msg$method))
}
value <- try(do.call(func, as.list(append(msg$args, msg$blobs))))
if (inherits(value, 'try-error')) {
.sendErrorResponse(msg, paste('Error:', as.character(value)))
}
else {
.sendResponse(msg, value)
}
},
.sendResponse = function(requestMsg, value) {
if (is.null(requestMsg$tag)) {
warning("Tried to send response for untagged message; method: ",
requestMsg$method)
return()
}
.write(toJSON(list(response=list(tag=requestMsg$tag, value=value))))
},
.sendErrorResponse = function(requestMsg, error) {
if (is.null(requestMsg$tag))
return()
.write(toJSON(list(response=list(tag=requestMsg$tag, error=error))))
},
.write = function(json) {
if (getOption('shiny.trace', FALSE))
message('SEND ', json)
if (getOption('shiny.transcode.json', TRUE))
json <- iconv(json, to='UTF-8')
websocket_write(json, .websocket)
},
# Public RPC methods
`@uploadInit` = function() {
return(list(jobId=.fileUploadContext$createUploadOperation()))
},
`@uploadFileBegin` = function(jobId, fileName, fileType, fileSize) {
.fileUploadContext$getUploadOperation(jobId)$fileBegin(list(
name=fileName, type=fileType, size=fileSize
))
invisible()
},
`@uploadFileChunk` = function(jobId, ...) {
args <- list(...)
if (length(args) != 1)
stop("Bad file chunk request")
.fileUploadContext$getUploadOperation(jobId)$fileChunk(args[[1]])
invisible()
},
`@uploadFileEnd` = function(jobId) {
.fileUploadContext$getUploadOperation(jobId)$fileEnd()
invisible()
},
`@uploadEnd` = function(jobId, inputId) {
fileData <- .fileUploadContext$getUploadOperation(jobId)$finish()
session$set(inputId, fileData)
invisible()
},
# Provides a mechanism for handling direct HTTP requests that are posted
# to the session (rather than going through the websocket)
handleRequest = function(ws, header, subpath) {
# TODO: Turn off caching for the response
matches <- regmatches(subpath,
regexec("^/([a-z]+)/([^?]*)",
subpath,
ignore.case=TRUE))[[1]]
if (length(matches) == 0)
return(httpResponse(400, 'text/html', '<h1>Bad Request</h1>'))
if (matches[2] == 'plot') {
savedPlot <- plots$get(utils::URLdecode(matches[3]))
if (is.null(savedPlot))
return(httpResponse(404, 'text/html', '<h1>Not Found</h1>'))
return(httpResponse(200, savedPlot$contentType, savedPlot$data))
}
if (matches[2] == 'download') {
# A bunch of ugliness here. Filenames can be dynamically generated by
# the user code, so we don't know what they'll be in advance. But the
# most reliable way to use non-ASCII filenames for downloads is to
# put the actual filename in the URL. So we will start with URLs in
# the form:
#
# /session/$TOKEN/download/$NAME
#
# When a request matching that pattern is received, we will calculate
# the filename and see if it's non-ASCII; if so, we'll redirect to
#
# /session/$TOKEN/download/$NAME/$FILENAME
#
# And when that pattern is received, we will actually return the file.
# Note that this means the filename and contents could be determined
# a few moments apart from each other (an HTTP roundtrip basically),
# hopefully that won't be enough to matter for anyone.
dlmatches <- regmatches(matches[3],
regexec("^([^/]+)(/[^/]+)?$",
matches[3]))[[1]]
dlname <- utils::URLdecode(dlmatches[2])
download <- downloads$get(dlname)
if (is.null(download))
return(httpResponse(404, 'text/html', '<h1>Not Found</h1>'))
filename <- ifelse(is.function(download$filename),
Context$new()$run(download$filename),
download$filename)
# If the URL does not contain the filename, and the desired filename
# contains non-ASCII characters, then do a redirect with the desired
# name tacked on the end.
if (dlmatches[3] == '' && grepl('[^ -~]', filename)) {
return(httpResponse(302, 'text/html', '<h1>Found</h1>', c(
'Location' = sprintf('%s/%s',
utils::URLencode(dlname, TRUE),
utils::URLencode(filename, TRUE)),
'Cache-Control' = 'no-cache')))
}
tmpdata <- tempfile()
on.exit(unlink(tmpdata))
result <- try(Context$new()$run(function() { download$func(tmpdata) }))
if (is(result, 'try-error')) {
return(httpResponse(500, 'text/plain',
attr(result, 'condition')$message))
}
return(httpResponse(
200,
download$contentType %OR% getContentType(tools::file_ext(filename)),
readBin(tmpdata, 'raw', n=file.info(tmpdata)$size),
c(
'Content-Disposition' = ifelse(
dlmatches[3] == '',
'attachment; filename="' %.%
gsub('(["\\\\])', '\\\\\\1', filename) %.% # yes, that many \'s
'"',
'attachment'
),
'Cache-Control'='no-cache')))
}
return(httpResponse(404, 'text/html', '<h1>Not Found</h1>'))
},
savePlot = function(name, data, contentType) {
plots$set(name, list(data=data, contentType=contentType))
return(sprintf('session/%s/plot/%s?%s',
URLencode(token, TRUE),
URLencode(name, TRUE),
createUniqueId(8)))
},
registerDownload = function(name, filename, contentType, func) {
downloads$set(name, list(filename = filename,
contentType = contentType,
func = func))
return(sprintf('session/%s/download/%s',
URLencode(token, TRUE),
URLencode(name, TRUE)))
}
)
)
@@ -117,8 +302,8 @@ resolve <- function(dir, relpath) {
abs.path <- file.path(dir, relpath)
if (!file.exists(abs.path))
return(NULL)
abs.path <- normalizePath(abs.path, winslash='/', mustWork=T)
dir <- normalizePath(dir, winslash='/', mustWork=T)
abs.path <- normalizePath(abs.path, winslash='/', mustWork=TRUE)
dir <- normalizePath(dir, winslash='/', mustWork=TRUE)
if (nchar(abs.path) <= nchar(dir) + 1)
return(NULL)
if (substr(abs.path, 1, nchar(dir)) != dir ||
@@ -128,14 +313,35 @@ resolve <- function(dir, relpath) {
return(abs.path)
}
httpResponse <- function(status = 200,
content_type = "text/html; charset=UTF-8",
content = "",
headers = c()) {
resp <- list(status = status, content_type = content_type, content = content,
headers = headers)
class(resp) <- 'httpResponse'
return(resp)
}
httpServer <- function(handlers) {
handler <- joinHandlers(handlers)
filter <- getOption('shiny.http.response.filter', NULL)
if (is.null(filter))
filter <- function(ws, header, response) response
function(ws, header) {
response <- handler(ws, header)
if (!is.null(response))
return(response)
else
return(http_response(ws, 404, content="<h1>Not Found</h1>"))
if (is.null(response))
response <- httpResponse(404, content="<h1>Not Found</h1>")
response <- filter(ws, header, response)
return(http_response(ws,
status=response$status,
content_type=response$content_type,
content=response$content,
headers=response$headers))
}
}
@@ -165,6 +371,25 @@ joinHandlers <- function(handlers) {
}
}
sessionHandler <- function(ws, header) {
path <- header$RESOURCE
if (is.null(path))
return(NULL)
matches <- regmatches(path, regexec('^/session/([0-9a-f]+)(/.*)$', path))
if (length(matches[[1]]) == 0)
return(NULL)
session <- matches[[1]][2]
subpath <- matches[[1]][3]
shinyapp <- appsByToken$get(session)
if (is.null(shinyapp))
return(NULL)
return(shinyapp$handleRequest(ws, header, subpath))
}
dynamicHandler <- function(filePath, dependencyFiles=filePath) {
lastKnownTimestamps <- NA
metaHandler <- function(ws, header) NULL
@@ -172,15 +397,21 @@ dynamicHandler <- function(filePath, dependencyFiles=filePath) {
if (!file.exists(filePath))
return(metaHandler)
cacheContext <- CacheContext$new()
return (function(ws, header) {
# Check if we need to rebuild
mtime <- file.info(dependencyFiles)$mtime
if (!identical(lastKnownTimestamps, mtime)) {
lastKnownTimestamps <<- mtime
if (cacheContext$isDirty()) {
cacheContext$reset()
for (dep in dependencyFiles)
cacheContext$addDependencyFile(dep)
clearClients()
if (file.exists(filePath)) {
local({
source(filePath, local=T)
cacheContext$with(function() {
source(filePath, local=new.env(parent=.GlobalEnv))
})
})
}
metaHandler <<- joinHandlers(.globals$clients)
@@ -196,7 +427,7 @@ staticHandler <- function(root) {
path <- header$RESOURCE
if (is.null(path))
return(http_response(ws, 400, content="<h1>Bad Request</h1>"))
return(httpResponse(400, content="<h1>Bad Request</h1>"))
if (path == '/')
path <- '/index.html'
@@ -206,22 +437,14 @@ staticHandler <- function(root) {
return(NULL)
ext <- tools::file_ext(abs.path)
content.type <- switch(ext,
html='text/html; charset=UTF-8',
htm='text/html; charset=UTF-8',
js='text/javascript',
css='text/css',
png='image/png',
jpg='image/jpeg',
jpeg='image/jpeg',
gif='image/gif',
'application/octet-stream')
content.type <- getContentType(ext)
response.content <- readBin(abs.path, 'raw', n=file.info(abs.path)$size)
return(http_response(ws, 200, content.type, response.content))
return(httpResponse(200, content.type, response.content))
})
}
apps <- Map$new()
appsByToken <- Map$new()
# Provide a character representation of the WS that can be used
# as a key in a Map.
@@ -243,6 +466,81 @@ registerClient <- function(client) {
.globals$clients <- append(.globals$clients, client)
}
.globals$resources <- list()
#' Resource Publishing
#'
#' Adds a directory of static resources to Shiny's web server, with the given
#' path prefix. Primarily intended for package authors to make supporting
#' JavaScript/CSS files available to their components.
#'
#' @param prefix The URL prefix (without slashes). Valid characters are a-z,
#' A-Z, 0-9, hyphen, and underscore; and must begin with a-z or A-Z. For
#' example, a value of 'foo' means that any request paths that begin with
#' '/foo' will be mapped to the given directory.
#' @param directoryPath The directory that contains the static resources to be
#' served.
#'
#' @details You can call \code{addResourcePath} multiple times for a given
#' \code{prefix}; only the most recent value will be retained. If the
#' normalized \code{directoryPath} is different than the directory that's
#' currently mapped to the \code{prefix}, a warning will be issued.
#'
#' @seealso \code{\link{singleton}}
#'
#' @examples
#' addResourcePath('datasets', system.file('data', package='datasets'))
#'
#' @export
addResourcePath <- function(prefix, directoryPath) {
prefix <- prefix[1]
if (!grepl('^[a-z][a-z0-9\\-_]*$', prefix, ignore.case=TRUE, perl=TRUE)) {
stop("addResourcePath called with invalid prefix; please see documentation")
}
if (prefix %in% c('shared')) {
stop("addResourcePath called with the reserved prefix '", prefix, "'; ",
"please use a different prefix")
}
directoryPath <- normalizePath(directoryPath, mustWork=TRUE)
existing <- .globals$resources[[prefix]]
if (!is.null(existing)) {
if (existing$directoryPath != directoryPath) {
warning("Overriding existing prefix ", prefix, " => ",
existing$directoryPath)
}
}
message('Shiny URLs starting with /', prefix, ' will mapped to ', directoryPath)
.globals$resources[[prefix]] <- list(directoryPath=directoryPath,
func=staticHandler(directoryPath))
}
resourcePathHandler <- function(ws, header) {
path <- header$RESOURCE
match <- regexpr('^/([^/]+)/', path, perl=TRUE)
if (match == -1)
return(NULL)
len <- attr(match, 'capture.length')
prefix <- substr(path, 2, 2 + len - 1)
resInfo <- .globals$resources[[prefix]]
if (is.null(resInfo))
return(NULL)
suffix <- substr(path, 2 + len, nchar(path))
header$RESOURCE <- suffix
return(resInfo$func(ws, header))
}
.globals$server <- NULL
#' Define Server Functionality
#'
@@ -281,16 +579,71 @@ shinyServer <- function(func) {
invisible()
}
decodeMessage <- function(data) {
readInt <- function(pos) {
packBits(rawToBits(data[pos:(pos+3)]), type='integer')
}
if (readInt(1) != 0x01020202L)
return(fromJSON(rawToChar(data), asText=TRUE, simplify=FALSE))
i <- 5
parts <- list()
while (i <= length(data)) {
length <- readInt(i)
i <- i + 4
if (length != 0)
parts <- append(parts, list(data[i:(i+length-1)]))
else
parts <- append(parts, list(raw(0)))
i <- i + length
}
mainMessage <- decodeMessage(parts[[1]])
mainMessage$blobs <- parts[2:length(parts)]
return(mainMessage)
}
# Takes a list-of-lists and returns a matrix. The lists
# must all be the same length. NULL is replaced by NA.
unpackMatrix <- function(data) {
if (length(data) == 0)
return(matrix(nrow=0, ncol=0))
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)
}
# Combine dir and (file)name into a file path. If a file already exists with a
# name differing only by case, then use it instead.
file.path.ci <- function(dir, name) {
default <- file.path(dir, name)
if (file.exists(default))
return(default)
if (!file.exists(dir))
return(default)
matches <- list.files(dir, name, ignore.case=TRUE, full.names=TRUE,
include.dirs=TRUE)
if (length(matches) == 0)
return(default)
return(matches[[1]])
}
# Instantiates the app in the current working directory.
# port - The TCP port that the application should listen on.
startApp <- function(port=8101L) {
sys.www.root <- system.file('www', package='shiny')
globalR <- file.path(getwd(), 'global.R')
uiR <- file.path(getwd(), 'ui.R')
serverR <- file.path(getwd(), 'server.R')
wwwDir <- file.path(getwd(), 'www')
globalR <- file.path.ci(getwd(), 'global.R')
uiR <- file.path.ci(getwd(), 'ui.R')
serverR <- file.path.ci(getwd(), 'server.R')
wwwDir <- file.path.ci(getwd(), 'www')
if (!file.exists(uiR) && !file.exists(wwwDir))
stop(paste("Neither ui.R nor a www subdirectory was found in", getwd()))
@@ -298,13 +651,13 @@ startApp <- function(port=8101L) {
stop(paste("server.R file was not found in", getwd()))
if (file.exists(globalR))
source(globalR, local=F)
source(globalR, local=FALSE)
shinyServer(NULL)
serverFileTimestamp <- NULL
local({
serverFileTimestamp <<- file.info(serverR)$mtime
source(serverR, local=T)
source(serverR, local=new.env(parent=.GlobalEnv))
if (is.null(.globals$server))
stop("No server was defined in server.R")
})
@@ -312,37 +665,60 @@ startApp <- function(port=8101L) {
ws_env <- create_server(
port=port,
webpage=httpServer(c(dynamicHandler(uiR), wwwDir, sys.www.root)))
webpage=httpServer(c(sessionHandler,
dynamicHandler(uiR),
wwwDir,
sys.www.root,
resourcePathHandler)))
set_callback('established', function(WS, ...) {
shinyapp <- ShinyApp$new(WS)
apps$set(wsToKey(WS), shinyapp)
appsByToken$set(shinyapp$token, shinyapp)
}, ws_env)
set_callback('closed', function(WS, ...) {
shinyapp <- apps$get(wsToKey(WS))
if (!is.null(shinyapp))
appsByToken$remove(shinyapp$token)
apps$remove(wsToKey(WS))
}, ws_env)
set_callback('receive', function(DATA, WS, ...) {
if (getOption('shiny.trace', F))
message("RECV ", rawToChar(DATA))
if (getOption('shiny.trace', FALSE)) {
if (as.raw(0) %in% DATA)
message("RECV ", '$$binary data$$')
else
message("RECV ", rawToChar(DATA))
}
if (identical(charToRaw("\003\xe9"), DATA))
return()
shinyapp <- apps$get(wsToKey(WS))
msg <- fromJSON(rawToChar(DATA), asText=T, simplify=F)
msg <- decodeMessage(DATA)
# Do our own list simplifying here. sapply/simplify2array give names to
# character vectors, which is rarely what we want.
if (!is.null(msg$data)) {
msg$data <- lapply(msg$data, function(x) {
if (is.list(x) && is.null(names(x)))
unlist(x, recursive=F)
else
x
})
for (name in names(msg$data)) {
val <- msg$data[[name]]
splitName <- strsplit(name, ':')[[1]]
if (length(splitName) > 1) {
msg$data[[name]] <- NULL
# TODO: Make the below a user-extensible registry of deserializers
msg$data[[ splitName[[1]] ]] <- switch(
splitName[[2]],
matrix = unpackMatrix(val),
stop('Unknown type specified for ', name)
)
}
else if (is.list(val) && is.null(names(val)))
msg$data[[name]] <- unlist(val, recursive=FALSE)
}
}
switch(
@@ -355,13 +731,15 @@ startApp <- function(port=8101L) {
shinyServer(NULL)
local({
serverFileTimestamp <<- mtime
source(serverR, local=T)
source(serverR, local=new.env(parent=.GlobalEnv))
if (is.null(.globals$server))
stop("No server was defined in server.R")
})
serverFunc <<- .globals$server
}
shinyapp$allowDataUriScheme <- msg$data[['__allowDataUriScheme']]
msg$data[['__allowDataUriScheme']] <- NULL
shinyapp$session$mset(msg$data)
flushReact()
local({
@@ -371,7 +749,9 @@ startApp <- function(port=8101L) {
},
update = {
shinyapp$session$mset(msg$data)
})
},
shinyapp$dispatch(msg)
)
flushReact()
shinyapp$flushOutput()
}, ws_env)
@@ -383,7 +763,7 @@ startApp <- function(port=8101L) {
# NOTE: we de-roxygenized this comment because the function isn't exported
# Run an application that was created by \code{\link{startApp}}. This
# function should normally be called in a \code{while(T)} loop.
# function should normally be called in a \code{while(TRUE)} loop.
#
# @param ws_env The return value from \code{\link{startApp}}.
serviceApp <- function(ws_env) {
@@ -423,10 +803,16 @@ runApp <- function(appDir=getwd(),
launch.browser=getOption('shiny.launch.browser',
interactive())) {
# Make warnings print immediately
ops <- options(warn = 1)
on.exit(options(ops))
orig.wd <- getwd()
setwd(appDir)
on.exit(setwd(orig.wd))
require(shiny)
ws_env <- startApp(port=port)
if (launch.browser) {
@@ -435,10 +821,11 @@ runApp <- function(appDir=getwd(),
}
tryCatch(
while (T) {
while (TRUE) {
serviceApp(ws_env)
},
finally = {
timerCallbacks$clear()
websocket_close(ws_env)
}
)
@@ -482,3 +869,102 @@ runExample <- function(example=NA,
runApp(dir, port = port, launch.browser = launch.browser)
}
}
# This is a wrapper for download.file and has the same interface.
# The only difference is that, if the protocol is https, it changes the
# download settings, depending on platform.
download <- function(url, ...) {
# First, check protocol. If https, check platform:
if (grepl('^https://', url)) {
# If Windows, call setInternet2, then use download.file with defaults.
if (.Platform$OS.type == "windows") {
# If we directly use setInternet2, R CMD CHECK gives a Note on Mac/Linux
mySI2 <- `::`(utils, 'setInternet2')
# Store initial settings
internet2_start <- mySI2(NA)
on.exit(mySI2(internet2_start))
# Needed for https
mySI2(TRUE)
download.file(url, ...)
} else {
# If non-Windows, check for curl/wget/lynx, then call download.file with
# appropriate method.
if (nzchar(Sys.which("wget")[1])) {
method <- "wget"
} else if (nzchar(Sys.which("curl")[1])) {
method <- "curl"
# curl needs to add a -L option to follow redirects.
# Save the original options and restore when we exit.
orig_extra_options <- getOption("download.file.extra")
on.exit(options(download.file.extra = orig_extra_options))
options(download.file.extra = paste("-L", orig_extra_options))
} else if (nzchar(Sys.which("lynx")[1])) {
method <- "lynx"
} else {
stop("no download method found")
}
download.file(url, method = method, ...)
}
} else {
download.file(url, ...)
}
}
#' Run a Shiny application from https://gist.github.com
#'
#' Download and launch a Shiny application that is hosted on GitHub as a gist.
#'
#' @param gist The identifier of the gist. For example, if the gist is
#' https://gist.github.com/3239667, then \code{3239667}, \code{'3239667'}, and
#' \code{'https://gist.github.com/3239667'} are all valid values.
#' @param port The TCP port that the application should listen on. Defaults to
#' port 8100.
#' @param launch.browser If true, the system's default web browser will be
#' launched automatically after the app is started. Defaults to true in
#' interactive sessions only.
#'
#' @export
runGist <- function(gist,
port=8100L,
launch.browser=getOption('shiny.launch.browser',
interactive())) {
gistUrl <- if (is.numeric(gist) || grepl('^[0-9a-f]+$', gist)) {
sprintf('https://gist.github.com/gists/%s/download', gist)
} else if(grepl('^https://gist.github.com/([0-9a-f]+)$', gist)) {
paste(sub('https://gist.github.com/',
'https://gist.github.com/gists/',
gist),
'/download',
sep='')
} else {
stop('Unrecognized gist identifier format')
}
filePath <- tempfile('shinygist', fileext='.tar.gz')
if (download(gistUrl, filePath, mode = "wb", quiet = TRUE) != 0)
stop("Failed to download URL ", gistUrl)
on.exit(unlink(filePath))
# Regular untar commonly causes two problems on Windows with github tarballs:
# 1) If RTools' tar.exe is in the path, you get cygwin path warnings which
# throw list=TRUE off;
# 2) If the internal untar implementation is used, it chokes on the 'g'
# type flag that github uses (to stash their commit hash info).
# By using our own forked/modified untar2 we sidestep both issues.
dirname <- untar2(filePath, list=TRUE)[1]
untar2(filePath, exdir = dirname(filePath))
appdir <- file.path(dirname(filePath), dirname)
on.exit(unlink(appdir, recursive = TRUE))
runApp(appdir, port=port, launch.browser=launch.browser)
}

View File

@@ -47,6 +47,31 @@ strong <- function(...) tags$strong(...)
#' @export
em <- function(...) tags$em(...)
#' @export
includeHTML <- function(path) {
dependsOnFile(path)
lines <- readLines(path, warn=FALSE, encoding='UTF-8')
return(HTML(paste(lines, collapse='\r\n')))
}
#' @export
includeText <- function(path) {
dependsOnFile(path)
lines <- readLines(path, warn=FALSE, encoding='UTF-8')
return(HTML(paste(lines, collapse='\r\n')))
}
#' @export
includeMarkdown <- function(path) {
if (!require(markdown))
stop("Markdown package is not installed")
dependsOnFile(path)
html <- markdown::markdownToHTML(path, fragment.only=TRUE)
Encoding(html) <- 'UTF-8'
return(HTML(html))
}
#' Include Content Only Once
#'
@@ -159,6 +184,8 @@ renderPage <- function(ui, connection) {
#' @export
shinyUI <- function(ui, path='/') {
force(ui)
registerClient({
function(ws, header) {
@@ -170,7 +197,7 @@ shinyUI <- function(ui, path='/') {
renderPage(ui, textConn)
html <- paste(textConnectionValue(textConn), collapse='\n')
return(http_response(ws, 200, content=html))
return(httpResponse(200, content=html))
}
})
}

View File

@@ -12,10 +12,16 @@ suppressPackageStartupMessages({
#' the CSS class name \code{shiny-plot-output}.
#'
#' @param func A function that generates a plot.
#' @param width The width of the rendered plot, in pixels; or \code{'auto'} to use
#' the \code{offsetWidth} of the HTML element that is bound to this plot.
#' @param height The height of the rendered plot, in pixels; or \code{'auto'} to use
#' the \code{offsetHeight} of the HTML element that is bound to this plot.
#' @param width The width of the rendered plot, in pixels; or \code{'auto'} to
#' use the \code{offsetWidth} of the HTML element that is bound to this plot.
#' You can also pass in a function that returns the width in pixels or
#' \code{'auto'}; in the body of the function you may reference reactive
#' values and functions.
#' @param height The height of the rendered plot, in pixels; or \code{'auto'} to
#' use the \code{offsetHeight} of the HTML element that is bound to this plot.
#' You can also pass in a function that returns the width in pixels or
#' \code{'auto'}; in the body of the function you may reference reactive
#' values and functions.
#' @param ... Arguments to be passed through to \code{\link[grDevices]{png}}.
#' These can be used to set the width, height, background color, etc.
#'
@@ -23,9 +29,19 @@ suppressPackageStartupMessages({
reactivePlot <- function(func, width='auto', height='auto', ...) {
args <- list(...)
if (is.function(width))
width <- reactive(width)
if (is.function(height))
height <- reactive(height)
return(function(shinyapp, name, ...) {
png.file <- tempfile(fileext='.png')
if (is.function(width))
width <- width()
if (is.function(height))
height <- height()
# Note that these are reactive calls. A change to the width and height
# will inherently cause a reactive plot to redraw (unless width and
# height were explicitly specified).
@@ -39,6 +55,7 @@ reactivePlot <- function(func, width='auto', height='auto', ...) {
return(NULL)
do.call(png, c(args, filename=png.file, width=width, height=height))
on.exit(unlink(png.file))
tryCatch(
func(),
finally=dev.off())
@@ -47,8 +64,15 @@ reactivePlot <- function(func, width='auto', height='auto', ...) {
if (is.na(bytes))
return(NULL)
b64 <- base64encode(readBin(png.file, 'raw', n=bytes))
return(paste("data:image/png;base64,", b64, sep=''))
pngData <- readBin(png.file, 'raw', n=bytes)
if (shinyapp$allowDataUriScheme) {
b64 <- base64encode(pngData)
return(paste("data:image/png;base64,", b64, sep=''))
}
else {
imageUrl <- shinyapp$savePlot(name, pngData, 'image/png')
return(imageUrl)
}
})
}
@@ -62,21 +86,26 @@ reactivePlot <- function(func, width='auto', height='auto', ...) {
#'
#' @param func A function that returns an R object that can be used with
#' \code{\link[xtable]{xtable}}.
#' @param ... Arguments to be passed through to \code{\link[xtable]{xtable}}.
#' @param ... Arguments to be passed through to \code{\link[xtable]{xtable}} and
#' \code{\link[xtable]{print.xtable}}.
#'
#' @export
reactiveTable <- function(func, ...) {
reactive(function() {
classNames <- getOption('shiny.table.class', 'data table table-bordered table-condensed')
data <- func()
if (is.null(data) || is.na(data))
return("")
return(paste(
capture.output(
print(xtable(data, ...),
type='html',
html.table.attributes=paste('class="',
htmlEscape(classNames, T),
htmlEscape(classNames, TRUE),
'"',
sep=''))),
sep=''), ...)),
collapse="\n"))
})
}
@@ -124,4 +153,82 @@ reactiveText <- function(func) {
reactive(function() {
return(paste(capture.output(cat(func())), collapse="\n"))
})
}
#' UI Output
#'
#' \bold{Experimental feature.} Makes a reactive version of a function that
#' generates HTML using the Shiny UI library.
#'
#' The corresponding HTML output tag should be \code{div} and have the CSS class
#' name \code{shiny-html-output} (or use \code{\link{uiOutput}}).
#'
#' @param func A function that returns a Shiny tag object, \code{\link{HTML}},
#' or a list of such objects.
#'
#' @seealso conditionalPanel
#'
#' @export
#' @examples
#' \dontrun{
#' output$moreControls <- reactiveUI(function() {
#' list(
#'
#' )
#' })
#' }
reactiveUI <- function(func) {
reactive(function() {
result <- func()
if (is.null(result) || length(result) == 0)
return(NULL)
return(as.character(result))
})
}
#' File Downloads
#'
#' Allows content from the Shiny application to be made available to the user as
#' file downloads (for example, downloading the currently visible data as a CSV
#' file). Both filename and contents can be calculated dynamically at the time
#' the user initiates the download. Assign the return value to a slot on
#' \code{output} in your server function, and in the UI use
#' \code{\link{downloadButton}} or \code{\link{downloadLink}} to make the
#' download available.
#'
#' @param filename A string of the filename, including extension, that the
#' user's web browser should default to when downloading the file; or a
#' function that returns such a string. (Reactive values and functions may be
#' used from this function.)
#' @param content A function that takes a single argument \code{file} that is a
#' file path (string) of a nonexistent temp file, and writes the content to
#' that file path. (Reactive values and functions may be used from this
#' function.)
#' @param contentType A string of the download's
#' \href{http://en.wikipedia.org/wiki/Internet_media_type}{content type}, for
#' example \code{"text/csv"} or \code{"image/png"}. If \code{NULL} or
#' \code{NA}, the content type will be guessed based on the filename
#' extension, or \code{application/octet-stream} if the extension is unknown.
#'
#' @examples
#' \dontrun{
#' # In server.R:
#' output$downloadData <- downloadHandler(
#' filename = function() {
#' paste('data-', Sys.Date(), '.csv', sep='')
#' },
#' content = function(file) {
#' write.csv(data, file)
#' }
#' )
#'
#' # In ui.R:
#' downloadLink('downloadData', 'Download')
#' }
#'
#' @export
downloadHandler <- function(filename, content, contentType=NA) {
return(function(shinyapp, name, ...) {
shinyapp$registerDownload(name, filename, contentType, content)
})
}

View File

@@ -70,7 +70,7 @@ slider <- function(inputId, min, max, value, step = NULL, ...,
}
# Default state is to not have ticks
if (identical(ticks, T)) {
if (identical(ticks, TRUE)) {
# Automatic ticks
tickCount <- (range / step) + 1
if (tickCount <= 26)
@@ -101,28 +101,18 @@ slider <- function(inputId, min, max, value, step = NULL, ...,
}
# build slider
sliderFragment <- list(
singleton(
tags$head(
tags$link(rel="stylesheet",
type="text/css",
href="shared/slider/css/jquery.slider.min.css"),
tags$script(src="shared/slider/js/jquery.slider.min.js")
)
),
tags$input(id=inputId, type="slider",
name=inputId, value=paste(value, collapse=';'), class="jslider",
'data-from'=min, 'data-to'=max, 'data-step'=step,
'data-skin'='plastic', 'data-round'=round, 'data-locale'=locale,
'data-format'=format, 'data-scale'=ticks,
'data-smooth'=FALSE)
)
if (identical(animate, T))
sliderFragment <- list(tags$input(
id=inputId, type="slider",
name=inputId, value=paste(value, collapse=';'), class="jslider",
'data-from'=min, 'data-to'=max, 'data-step'=step,
'data-skin'='plastic', 'data-round'=round, 'data-locale'=locale,
'data-format'=format, 'data-scale'=ticks,
'data-smooth'=FALSE))
if (identical(animate, TRUE))
animate <- animationOptions()
if (!is.null(animate) && !identical(animate, F)) {
if (!is.null(animate) && !identical(animate, FALSE)) {
if (is.null(animate$playButton))
animate$playButton <- 'Play'
if (is.null(animate$pauseButton))

View File

@@ -16,7 +16,7 @@ htmlEscape <- local({
)
.htmlSpecialsPatternAttrib <- paste(names(.htmlSpecialsAttrib), collapse='|')
function(text, attribute=T) {
function(text, attribute=TRUE) {
pattern <- if(attribute)
.htmlSpecialsPatternAttrib
else
@@ -32,7 +32,7 @@ htmlEscape <- local({
.htmlSpecials
for (chr in names(specials)) {
text <- gsub(chr, specials[[chr]], text, fixed=T)
text <- gsub(chr, specials[[chr]], text, fixed=TRUE)
}
return(text)
@@ -64,6 +64,15 @@ as.character.shiny.tag <- function(x, ...) {
return(HTML(paste(readLines(f), collapse='\n')))
}
#' @S3method print shiny.tag.list
print.shiny.tag.list <- print.shiny.tag
#' @S3method format shiny.tag.list
format.shiny.tag.list <- format.shiny.tag
#' @S3method as.character shiny.tag.list
as.character.shiny.tag.list <- as.character.shiny.tag
normalizeText <- function(text) {
if (!is.null(attr(text, "html")))
text
@@ -72,6 +81,13 @@ normalizeText <- function(text) {
}
#' @export
tagList <- function(...) {
lst <- list(...)
class(lst) <- c("shiny.tag.list", "list")
return(lst)
}
#' @export
tagAppendChild <- function(tag, child) {
tag$children[[length(tag$children)+1]] <- child

191
R/tar.R Normal file
View File

@@ -0,0 +1,191 @@
# This file was pulled from the R code base as of
# Thursday, November 22, 2012 at 6:24:55 AM UTC
# and edited to remove everything but the copyright
# header and untar2, and to make untar2 more tolerant
# of the 'x' and 'g' extended block indicators, the
# latter of which is used in tar files generated by
# GitHub.
# File src/library/utils/R/tar.R
# Part of the R package, http://www.R-project.org
#
# Copyright (C) 1995-2012 The R Core Team
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# A copy of the GNU General Public License is available at
# http://www.r-project.org/Licenses/
untar2 <- function(tarfile, files = NULL, list = FALSE, exdir = ".")
{
getOct <- function(x, offset, len)
{
x <- 0L
for(i in offset + seq_len(len)) {
z <- block[i]
if(!as.integer(z)) break; # terminate on nul
switch(rawToChar(z),
" " = {},
"0"=,"1"=,"2"=,"3"=,"4"=,"5"=,"6"=,"7"=
{x <- 8*x + (as.integer(z)-48)},
stop("invalid octal digit")
)
}
x
}
mydir.create <- function(path, ...) {
## for Windows' sake
path <- sub("[\\/]$", "", path)
if(file_test("-d", path)) return()
if(!dir.create(path, showWarnings = TRUE, recursive = TRUE, ...))
stop(gettextf("failed to create directory %s", sQuote(path)),
domain = NA)
}
warn1 <- character()
## A tar file is a set of 512 byte records,
## a header record followed by file contents (zero-padded).
## See http://en.wikipedia.org/wiki/Tar_%28file_format%29
if(is.character(tarfile) && length(tarfile) == 1L) {
con <- gzfile(path.expand(tarfile), "rb") # reads compressed formats
on.exit(close(con))
} else if(inherits(tarfile, "connection")) con <- tarfile
else stop("'tarfile' must be a character string or a connection")
if (!missing(exdir)) {
mydir.create(exdir)
od <- setwd(exdir)
on.exit(setwd(od), add = TRUE)
}
contents <- character()
llink <- lname <- NULL
repeat{
block <- readBin(con, "raw", n = 512L)
if(!length(block)) break
if(length(block) < 512L) stop("incomplete block on file")
if(all(block == 0)) break
ns <- max(which(block[1:100] > 0))
name <- rawToChar(block[seq_len(ns)])
magic <- rawToChar(block[258:262])
if ((magic == "ustar") && block[346] > 0) {
ns <- max(which(block[346:500] > 0))
prefix <- rawToChar(block[345+seq_len(ns)])
name <- file.path(prefix, name)
}
## mode zero-padded 8 bytes (including nul) at 101
## Aargh: bsdtar has this one incorrectly with 6 bytes+space
mode <- as.octmode(getOct(block, 100, 8))
size <- getOct(block, 124, 12)
ts <- getOct(block, 136, 12)
ft <- as.POSIXct(as.numeric(ts), origin="1970-01-01", tz="UTC")
csum <- getOct(block, 148, 8)
block[149:156] <- charToRaw(" ")
xx <- as.integer(block)
checksum <- sum(xx) %% 2^24 # 6 bytes
if(csum != checksum) {
## try it with signed bytes.
checksum <- sum(ifelse(xx > 127, xx - 128, xx)) %% 2^24 # 6 bytes
if(csum != checksum)
warning(gettextf("checksum error for entry '%s'", name),
domain = NA)
}
type <- block[157L]
ctype <- rawToChar(type)
if(type == 0L || ctype == "0") {
if(!is.null(lname)) {name <- lname; lname <- NULL}
contents <- c(contents, name)
remain <- size
dothis <- !list
if(dothis && length(files)) dothis <- name %in% files
if(dothis) {
mydir.create(dirname(name))
out <- file(name, "wb")
}
for(i in seq_len(ceiling(size/512L))) {
block <- readBin(con, "raw", n = 512L)
if(length(block) < 512L)
stop("incomplete block on file")
if (dothis) {
writeBin(block[seq_len(min(512L, remain))], out)
remain <- remain - 512L
}
}
if(dothis) {
close(out)
Sys.chmod(name, mode, FALSE) # override umask
Sys.setFileTime(name, ft)
}
} else if(ctype %in% c("1", "2")) { # hard and symbolic links
contents <- c(contents, name)
ns <- max(which(block[158:257] > 0))
name2 <- rawToChar(block[157L + seq_len(ns)])
if(!is.null(lname)) {name <- lname; lname <- NULL}
if(!is.null(llink)) {name2 <- llink; llink <- NULL}
if(!list) {
if(ctype == "1") {
if (!file.link(name2, name)) { # will give a warning
## link failed, so try a file copy
if(file.copy(name2, name))
warn1 <- c(warn1, "restoring hard link as a file copy")
else
warning(gettextf("failed to copy %s to %s", sQuote(name2), sQuote(name)), domain = NA)
}
} else {
if(.Platform$OS.type == "windows") {
## this will not work for links to dirs
from <- file.path(dirname(name), name2)
if (!file.copy(from, name))
warning(gettextf("failed to copy %s to %s", sQuote(from), sQuote(name)), domain = NA)
else
warn1 <- c(warn1, "restoring symbolic link as a file copy")
} else {
if(!file.symlink(name2, name)) { # will give a warning
## so try a file copy: will not work for links to dirs
from <- file.path(dirname(name), name2)
if (file.copy(from, name))
warn1 <- c(warn1, "restoring symbolic link as a file copy")
else
warning(gettextf("failed to copy %s to %s", sQuote(from), sQuote(name)), domain = NA)
}
}
}
}
} else if(ctype == "5") {
contents <- c(contents, name)
if(!list) {
mydir.create(name)
Sys.chmod(name, mode, TRUE) # FIXME: check result
## no point is setting time, as dir will be populated later.
}
} else if(ctype %in% c("L", "K")) {
## This is a GNU extension that should no longer be
## in use, but it is.
name_size <- 512L * ceiling(size/512L)
block <- readBin(con, "raw", n = name_size)
if(length(block) < name_size)
stop("incomplete block on file")
ns <- max(which(block > 0)) # size on file may or may not include final nul
if(ctype == "L")
lname <- rawToChar(block[seq_len(ns)])
else
llink <- rawToChar(block[seq_len(ns)])
} else if(ctype %in% c("x", "g")) {
readBin(con, "raw", n = 512L*ceiling(size/512L))
} else stop("unsupported entry type ", sQuote(ctype))
}
if(length(warn1)) {
warn1 <- unique(warn1)
for (w in warn1) warning(w, domain = NA)
}
if(list) contents else invisible(0L)
}

View File

@@ -15,6 +15,11 @@ TimerCallbacks <- setRefClass(
initialize = function() {
.nextId <<- 0L
},
clear = function() {
.nextId <<- 0L
.funcs$clear()
.times <<- data.frame()
},
schedule = function(millis, func) {
id <- .nextId
.nextId <<- .nextId + 1L
@@ -51,7 +56,7 @@ TimerCallbacks <- setRefClass(
executeElapsed = function() {
elapsed <- takeElapsed()
if (length(elapsed) == 0)
return(F)
return(FALSE)
for (id in elapsed$id) {
thisFunc <- .funcs$remove(as.character(id))
@@ -59,7 +64,7 @@ TimerCallbacks <- setRefClass(
# TODO: Detect NULL, and...?
thisFunc()
}
return(T)
return(TRUE)
}
)
)

104
R/utils.R Normal file
View File

@@ -0,0 +1,104 @@
#' Make a random number generator repeatable
#'
#' Given a function that generates random data, returns a wrapped version of
#' that function that always uses the same seed when called. The seed to use can
#' be passed in explicitly if desired; otherwise, a random number is used.
#'
#' @param rngfunc The function that is affected by the R session's seed.
#' @param seed The seed to set every time the resulting function is called.
#' @return A repeatable version of the function that was passed in.
#'
#' @note When called, the returned function attempts to preserve the R session's
#' current seed by snapshotting and restoring
#' \code{\link[base]{.Random.seed}}.
#'
#' @examples
#' rnormA <- repeatable(rnorm)
#' rnormB <- repeatable(rnorm)
#' rnormA(3) # [1] 1.8285879 -0.7468041 -0.4639111
#' rnormA(3) # [1] 1.8285879 -0.7468041 -0.4639111
#' rnormA(5) # [1] 1.8285879 -0.7468041 -0.4639111 -1.6510126 -1.4686924
#' rnormB(5) # [1] -0.7946034 0.2568374 -0.6567597 1.2451387 -0.8375699
#'
#' @export
repeatable <- function(rngfunc, seed = runif(1, 0, .Machine$integer.max)) {
force(seed)
function(...) {
# When we exit, restore the seed to its original state
if (exists('.Random.seed', where=globalenv())) {
currentSeed <- get('.Random.seed', pos=globalenv())
on.exit(assign('.Random.seed', currentSeed, pos=globalenv()))
}
else {
on.exit(rm('.Random.seed', pos=globalenv()))
}
set.seed(seed)
do.call(rngfunc, list(...))
}
}
`%OR%` <- function(x, y) {
ifelse(is.null(x) || is.na(x), y, x)
}
`%AND%` <- function(x, y) {
if (!is.null(x) && !is.na(x))
if (!is.null(y) && !is.na(y))
return(y)
return(NULL)
}
`%.%` <- function(x, y) {
paste(x, y, sep='')
}
knownContentTypes <- Map$new()
knownContentTypes$mset(
html='text/html; charset=UTF-8',
htm='text/html; charset=UTF-8',
js='text/javascript',
css='text/css',
png='image/png',
jpg='image/jpeg',
jpeg='image/jpeg',
gif='image/gif',
svg='image/svg+xml',
txt='text/plain',
pdf='application/pdf',
ps='application/postscript',
xml='application/xml',
m3u='audio/x-mpegurl',
m4a='audio/mp4a-latm',
m4b='audio/mp4a-latm',
m4p='audio/mp4a-latm',
mp3='audio/mpeg',
wav='audio/x-wav',
m4u='video/vnd.mpegurl',
m4v='video/x-m4v',
mp4='video/mp4',
mpeg='video/mpeg',
mpg='video/mpeg',
avi='video/x-msvideo',
mov='video/quicktime',
ogg='application/ogg',
swf='application/x-shockwave-flash',
doc='application/msword',
xls='application/vnd.ms-excel',
ppt='application/vnd.ms-powerpoint',
xlsx='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
xltx='application/vnd.openxmlformats-officedocument.spreadsheetml.template',
potx='application/vnd.openxmlformats-officedocument.presentationml.template',
ppsx='application/vnd.openxmlformats-officedocument.presentationml.slideshow',
pptx='application/vnd.openxmlformats-officedocument.presentationml.presentation',
sldx='application/vnd.openxmlformats-officedocument.presentationml.slide',
docx='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
dotx='application/vnd.openxmlformats-officedocument.wordprocessingml.template',
xlam='application/vnd.ms-excel.addin.macroEnabled.12',
xlsb='application/vnd.ms-excel.sheet.binary.macroEnabled.12')
getContentType <- function(ext, defaultType='application/octet-stream') {
knownContentTypes$get(tolower(ext)) %OR% defaultType
}

View File

@@ -2,6 +2,8 @@
Shiny is a new package from RStudio that makes it incredibly easy to build interactive web applications with R.
For an introduction and examples, visit the [Shiny homepage](http://www.rstudio.com/shiny/).
## Features
* Build useful web applications with only a few lines of code&mdash;no JavaScript required.

View File

@@ -10,10 +10,10 @@ shinyUI(pageWithSidebar(
# and to specify whether outliers should be included
sidebarPanel(
selectInput("variable", "Variable:",
list("Cylinders" = "cyl",
"Transmission" = "am",
"Gears" = "gear")),
c("Cylinders" = "cyl",
"Transmission" = "am",
"Gears" = "gear")),
checkboxInput("outliers", "Show outliers", FALSE)
),

View File

@@ -27,7 +27,7 @@ shinyUI(pageWithSidebar(
# Animation with custom interval (in ms) to control speed, plus looping
sliderInput("animation", "Looping Animation:", 1, 2000, 1, step = 10,
animate=animationOptions(interval=300, loop=T))
animate=animationOptions(interval=300, loop=TRUE))
),
# Show a table summarizing the values entered

View File

@@ -11,10 +11,10 @@ shinyUI(pageWithSidebar(
# element to introduce extra vertical spacing
sidebarPanel(
radioButtons("dist", "Distribution type:",
list("Normal" = "norm",
"Uniform" = "unif",
"Log-normal" = "lnorm",
"Exponential" = "exp")),
c("Normal" = "norm",
"Uniform" = "unif",
"Log-normal" = "lnorm",
"Exponential" = "exp")),
br(),
sliderInput("n",

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
shinyUI(pageWithSidebar(
headerPanel('Downloading Data'),
sidebarPanel(
selectInput("dataset", "Choose a dataset:",
choices = c("rock", "pressure", "cars")),
downloadButton('downloadData', 'Download')
),
mainPanel(
tableOutput('table')
)
))

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,5 +1,5 @@
/* ===================================================
* bootstrap-transition.js v2.0.4
* bootstrap-transition.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#transitions
* ===================================================
* Copyright 2012 Twitter, Inc.
@@ -36,8 +36,7 @@
, transEndEventNames = {
'WebkitTransition' : 'webkitTransitionEnd'
, 'MozTransition' : 'transitionend'
, 'OTransition' : 'oTransitionEnd'
, 'msTransition' : 'MSTransitionEnd'
, 'OTransition' : 'oTransitionEnd otransitionend'
, 'transition' : 'transitionend'
}
, name
@@ -59,7 +58,7 @@
})
}(window.jQuery);/* ==========================================================
* bootstrap-alert.js v2.0.4
* bootstrap-alert.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#alerts
* ==========================================================
* Copyright 2012 Twitter, Inc.
@@ -148,7 +147,7 @@
})
}(window.jQuery);/* ============================================================
* bootstrap-button.js v2.0.4
* bootstrap-button.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#buttons
* ============================================================
* Copyright 2012 Twitter, Inc.
@@ -243,7 +242,7 @@
})
}(window.jQuery);/* ==========================================================
* bootstrap-carousel.js v2.0.4
* bootstrap-carousel.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#carousel
* ==========================================================
* Copyright 2012 Twitter, Inc.
@@ -290,7 +289,7 @@
}
, to: function (pos) {
var $active = this.$element.find('.active')
var $active = this.$element.find('.item.active')
, children = $active.parent().children()
, activePos = children.index($active)
, that = this
@@ -312,6 +311,10 @@
, pause: function (e) {
if (!e) this.paused = true
if (this.$element.find('.next, .prev').length && $.support.transition.end) {
this.$element.trigger($.support.transition.end)
this.cycle()
}
clearInterval(this.interval)
this.interval = null
return this
@@ -328,13 +331,15 @@
}
, slide: function (type, next) {
var $active = this.$element.find('.active')
var $active = this.$element.find('.item.active')
, $next = next || $active[type]()
, isCycling = this.interval
, direction = type == 'next' ? 'left' : 'right'
, fallback = type == 'next' ? 'first' : 'last'
, that = this
, e = $.Event('slide')
, e = $.Event('slide', {
relatedTarget: $next[0]
})
this.sliding = true
@@ -382,9 +387,10 @@
var $this = $(this)
, data = $this.data('carousel')
, options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option)
, action = typeof option == 'string' ? option : options.slide
if (!data) $this.data('carousel', (data = new Carousel(this, options)))
if (typeof option == 'number') data.to(option)
else if (typeof option == 'string' || (option = options.slide)) data[option]()
else if (action) data[action]()
else if (options.interval) data.cycle()
})
}
@@ -411,7 +417,7 @@
})
}(window.jQuery);/* =============================================================
* bootstrap-collapse.js v2.0.4
* bootstrap-collapse.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#collapse
* =============================================================
* Copyright 2012 Twitter, Inc.
@@ -479,7 +485,7 @@
this.$element[dimension](0)
this.transition('addClass', $.Event('show'), 'shown')
this.$element[dimension](this.$element[0][scroll])
$.support.transition && this.$element[dimension](this.$element[0][scroll])
}
, hide: function () {
@@ -556,18 +562,19 @@
* ==================== */
$(function () {
$('body').on('click.collapse.data-api', '[data-toggle=collapse]', function ( e ) {
$('body').on('click.collapse.data-api', '[data-toggle=collapse]', function (e) {
var $this = $(this), href
, target = $this.attr('data-target')
|| e.preventDefault()
|| (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7
, option = $(target).data('collapse') ? 'toggle' : $this.data()
$this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed')
$(target).collapse(option)
})
})
}(window.jQuery);/* ============================================================
* bootstrap-dropdown.js v2.0.4
* bootstrap-dropdown.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#dropdowns
* ============================================================
* Copyright 2012 Twitter, Inc.
@@ -594,7 +601,7 @@
/* DROPDOWN CLASS DEFINITION
* ========================= */
var toggle = '[data-toggle="dropdown"]'
var toggle = '[data-toggle=dropdown]'
, Dropdown = function (element) {
var $el = $(element).on('click.dropdown.data-api', this.toggle)
$('html').on('click.dropdown.data-api', function () {
@@ -609,34 +616,82 @@
, toggle: function (e) {
var $this = $(this)
, $parent
, selector
, isActive
if ($this.is('.disabled, :disabled')) return
selector = $this.attr('data-target')
if (!selector) {
selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
}
$parent = $(selector)
$parent.length || ($parent = $this.parent())
$parent = getParent($this)
isActive = $parent.hasClass('open')
clearMenus()
if (!isActive) $parent.toggleClass('open')
if (!isActive) {
$parent.toggleClass('open')
$this.focus()
}
return false
}
, keydown: function (e) {
var $this
, $items
, $active
, $parent
, isActive
, index
if (!/(38|40|27)/.test(e.keyCode)) return
$this = $(this)
e.preventDefault()
e.stopPropagation()
if ($this.is('.disabled, :disabled')) return
$parent = getParent($this)
isActive = $parent.hasClass('open')
if (!isActive || (isActive && e.keyCode == 27)) return $this.click()
$items = $('[role=menu] li:not(.divider) a', $parent)
if (!$items.length) return
index = $items.index($items.filter(':focus'))
if (e.keyCode == 38 && index > 0) index-- // up
if (e.keyCode == 40 && index < $items.length - 1) index++ // down
if (!~index) index = 0
$items
.eq(index)
.focus()
}
}
function clearMenus() {
$(toggle).parent().removeClass('open')
getParent($(toggle))
.removeClass('open')
}
function getParent($this) {
var selector = $this.attr('data-target')
, $parent
if (!selector) {
selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
}
$parent = $(selector)
$parent.length || ($parent = $this.parent())
return $parent
}
@@ -659,14 +714,16 @@
* =================================== */
$(function () {
$('html').on('click.dropdown.data-api', clearMenus)
$('html')
.on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus)
$('body')
.on('click.dropdown', '.dropdown form', function (e) { e.stopPropagation() })
.on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle)
.on('click.dropdown touchstart.dropdown.data-api', '.dropdown', function (e) { e.stopPropagation() })
.on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle)
.on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
})
}(window.jQuery);/* =========================================================
* bootstrap-modal.js v2.0.4
* bootstrap-modal.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#modals
* =========================================================
* Copyright 2012 Twitter, Inc.
@@ -693,10 +750,11 @@
/* MODAL CLASS DEFINITION
* ====================== */
var Modal = function (content, options) {
var Modal = function (element, options) {
this.options = options
this.$element = $(content)
this.$element = $(element)
.delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this))
this.options.remote && this.$element.find('.modal-body').load(this.options.remote)
}
Modal.prototype = {
@@ -719,8 +777,9 @@
this.isShown = true
escape.call(this)
backdrop.call(this, function () {
this.escape()
this.backdrop(function () {
var transition = $.support.transition && that.$element.hasClass('fade')
if (!that.$element.parent().length) {
@@ -734,7 +793,12 @@
that.$element[0].offsetWidth // force reflow
}
that.$element.addClass('in')
that.$element
.addClass('in')
.attr('aria-hidden', false)
.focus()
that.enforceFocus()
transition ?
that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) :
@@ -758,90 +822,98 @@
$('body').removeClass('modal-open')
escape.call(this)
this.escape()
this.$element.removeClass('in')
$(document).off('focusin.modal')
this.$element
.removeClass('in')
.attr('aria-hidden', true)
$.support.transition && this.$element.hasClass('fade') ?
hideWithTransition.call(this) :
hideModal.call(this)
this.hideWithTransition() :
this.hideModal()
}
}
/* MODAL PRIVATE METHODS
* ===================== */
function hideWithTransition() {
var that = this
, timeout = setTimeout(function () {
that.$element.off($.support.transition.end)
hideModal.call(that)
}, 500)
this.$element.one($.support.transition.end, function () {
clearTimeout(timeout)
hideModal.call(that)
})
}
function hideModal(that) {
this.$element
.hide()
.trigger('hidden')
backdrop.call(this)
}
function backdrop(callback) {
var that = this
, animate = this.$element.hasClass('fade') ? 'fade' : ''
if (this.isShown && this.options.backdrop) {
var doAnimate = $.support.transition && animate
this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
.appendTo(document.body)
if (this.options.backdrop != 'static') {
this.$backdrop.click($.proxy(this.hide, this))
, enforceFocus: function () {
var that = this
$(document).on('focusin.modal', function (e) {
if (that.$element[0] !== e.target && !that.$element.has(e.target).length) {
that.$element.focus()
}
})
}
if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
, escape: function () {
var that = this
if (this.isShown && this.options.keyboard) {
this.$element.on('keyup.dismiss.modal', function ( e ) {
e.which == 27 && that.hide()
})
} else if (!this.isShown) {
this.$element.off('keyup.dismiss.modal')
}
}
this.$backdrop.addClass('in')
, hideWithTransition: function () {
var that = this
, timeout = setTimeout(function () {
that.$element.off($.support.transition.end)
that.hideModal()
}, 500)
doAnimate ?
this.$backdrop.one($.support.transition.end, callback) :
callback()
this.$element.one($.support.transition.end, function () {
clearTimeout(timeout)
that.hideModal()
})
}
} else if (!this.isShown && this.$backdrop) {
this.$backdrop.removeClass('in')
, hideModal: function (that) {
this.$element
.hide()
.trigger('hidden')
$.support.transition && this.$element.hasClass('fade')?
this.$backdrop.one($.support.transition.end, $.proxy(removeBackdrop, this)) :
removeBackdrop.call(this)
this.backdrop()
}
} else if (callback) {
callback()
}
}
, removeBackdrop: function () {
this.$backdrop.remove()
this.$backdrop = null
}
function removeBackdrop() {
this.$backdrop.remove()
this.$backdrop = null
}
, backdrop: function (callback) {
var that = this
, animate = this.$element.hasClass('fade') ? 'fade' : ''
function escape() {
var that = this
if (this.isShown && this.options.keyboard) {
$(document).on('keyup.dismiss.modal', function ( e ) {
e.which == 27 && that.hide()
})
} else if (!this.isShown) {
$(document).off('keyup.dismiss.modal')
}
if (this.isShown && this.options.backdrop) {
var doAnimate = $.support.transition && animate
this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
.appendTo(document.body)
if (this.options.backdrop != 'static') {
this.$backdrop.click($.proxy(this.hide, this))
}
if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
this.$backdrop.addClass('in')
doAnimate ?
this.$backdrop.one($.support.transition.end, callback) :
callback()
} else if (!this.isShown && this.$backdrop) {
this.$backdrop.removeClass('in')
$.support.transition && this.$element.hasClass('fade')?
this.$backdrop.one($.support.transition.end, $.proxy(this.removeBackdrop, this)) :
this.removeBackdrop()
} else if (callback) {
callback()
}
}
}
@@ -873,17 +945,23 @@
$(function () {
$('body').on('click.modal.data-api', '[data-toggle="modal"]', function ( e ) {
var $this = $(this), href
, $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
, option = $target.data('modal') ? 'toggle' : $.extend({}, $target.data(), $this.data())
var $this = $(this)
, href = $this.attr('href')
, $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
, option = $target.data('modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
e.preventDefault()
$target.modal(option)
$target
.modal(option)
.one('hide', function () {
$this.focus()
})
})
})
}(window.jQuery);/* ===========================================================
* bootstrap-tooltip.js v2.0.4
* bootstrap-tooltip.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#tooltips
* Inspired by the original jQuery.tipsy by Jason Frame
* ===========================================================
@@ -928,11 +1006,13 @@
this.options = this.getOptions(options)
this.enabled = true
if (this.options.trigger != 'manual') {
eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus'
if (this.options.trigger == 'click') {
this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
} else if (this.options.trigger != 'manual') {
eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus'
eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur'
this.$element.on(eventIn, this.options.selector, $.proxy(this.enter, this))
this.$element.on(eventOut, this.options.selector, $.proxy(this.leave, this))
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
}
this.options.selector ?
@@ -1032,20 +1112,11 @@
}
}
, isHTML: function(text) {
// html string detection logic adapted from jQuery
return typeof text != 'string'
|| ( text.charAt(0) === "<"
&& text.charAt( text.length - 1 ) === ">"
&& text.length >= 3
) || /^(?:[^<]*<[\w\W]+>[^>]*$)/.exec(text)
}
, setContent: function () {
var $tip = this.tip()
, title = this.getTitle()
$tip.find('.tooltip-inner')[this.isHTML(title) ? 'html' : 'text'](title)
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
$tip.removeClass('fade in top bottom left right')
}
@@ -1069,6 +1140,8 @@
$.support.transition && this.$tip.hasClass('fade') ?
removeWithAnimation() :
$tip.remove()
return this
}
, fixTitle: function () {
@@ -1128,6 +1201,10 @@
this[this.tip().hasClass('in') ? 'hide' : 'show']()
}
, destroy: function () {
this.hide().$element.off('.' + this.type).removeData(this.type)
}
}
@@ -1154,11 +1231,12 @@
, trigger: 'hover'
, title: ''
, delay: 0
, html: true
}
}(window.jQuery);
/* ===========================================================
* bootstrap-popover.js v2.0.4
* bootstrap-popover.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#popovers
* ===========================================================
* Copyright 2012 Twitter, Inc.
@@ -1185,7 +1263,7 @@
/* POPOVER PUBLIC CLASS DEFINITION
* =============================== */
var Popover = function ( element, options ) {
var Popover = function (element, options) {
this.init('popover', element, options)
}
@@ -1202,8 +1280,8 @@
, title = this.getTitle()
, content = this.getContent()
$tip.find('.popover-title')[this.isHTML(title) ? 'html' : 'text'](title)
$tip.find('.popover-content > *')[this.isHTML(content) ? 'html' : 'text'](content)
$tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
$tip.find('.popover-content > *')[this.options.html ? 'html' : 'text'](content)
$tip.removeClass('fade top bottom left right in')
}
@@ -1230,6 +1308,10 @@
return this.$tip
}
, destroy: function () {
this.hide().$element.off('.' + this.type).removeData(this.type)
}
})
@@ -1250,12 +1332,13 @@
$.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, {
placement: 'right'
, trigger: 'click'
, content: ''
, template: '<div class="popover"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>'
})
}(window.jQuery);/* =============================================================
* bootstrap-scrollspy.js v2.0.4
* bootstrap-scrollspy.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#scrollspy
* =============================================================
* Copyright 2012 Twitter, Inc.
@@ -1279,15 +1362,15 @@
"use strict"; // jshint ;_;
/* SCROLLSPY CLASS DEFINITION
* ========================== */
/* SCROLLSPY CLASS DEFINITION
* ========================== */
function ScrollSpy( element, options) {
function ScrollSpy(element, options) {
var process = $.proxy(this.process, this)
, $element = $(element).is('body') ? $(window) : $(element)
, href
this.options = $.extend({}, $.fn.scrollspy.defaults, options)
this.$scrollElement = $element.on('scroll.scroll.data-api', process)
this.$scrollElement = $element.on('scroll.scroll-spy.data-api', process)
this.selector = (this.options.target
|| ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
|| '') + ' .nav li > a'
@@ -1314,7 +1397,7 @@
, href = $el.data('target') || $el.attr('href')
, $href = /^#\w/.test(href) && $(href)
return ( $href
&& href.length
&& $href.length
&& [[ $href.position().top, href ]] ) || null
})
.sort(function (a, b) { return a[0] - b[0] })
@@ -1364,7 +1447,7 @@
.parent('li')
.addClass('active')
if (active.parent('.dropdown-menu')) {
if (active.parent('.dropdown-menu').length) {
active = active.closest('li.dropdown').addClass('active')
}
@@ -1377,7 +1460,7 @@
/* SCROLLSPY PLUGIN DEFINITION
* =========================== */
$.fn.scrollspy = function ( option ) {
$.fn.scrollspy = function (option) {
return this.each(function () {
var $this = $(this)
, data = $this.data('scrollspy')
@@ -1397,7 +1480,7 @@
/* SCROLLSPY DATA-API
* ================== */
$(function () {
$(window).on('load', function () {
$('[data-spy="scroll"]').each(function () {
var $spy = $(this)
$spy.scrollspy($spy.data())
@@ -1405,7 +1488,7 @@
})
}(window.jQuery);/* ========================================================
* bootstrap-tab.js v2.0.4
* bootstrap-tab.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#tabs
* ========================================================
* Copyright 2012 Twitter, Inc.
@@ -1432,7 +1515,7 @@
/* TAB CLASS DEFINITION
* ==================== */
var Tab = function ( element ) {
var Tab = function (element) {
this.element = $(element)
}
@@ -1539,7 +1622,7 @@
})
}(window.jQuery);/* =============================================================
* bootstrap-typeahead.js v2.0.4
* bootstrap-typeahead.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#typeahead
* =============================================================
* Copyright 2012 Twitter, Inc.
@@ -1617,17 +1700,23 @@
}
, lookup: function (event) {
var that = this
, items
, q
var items
this.query = this.$element.val()
if (!this.query) {
if (!this.query || this.query.length < this.options.minLength) {
return this.shown ? this.hide() : this
}
items = $.grep(this.source, function (item) {
items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
return items ? this.process(items) : this
}
, process: function (items) {
var that = this
items = $.grep(items, function (item) {
return that.matcher(item)
})
@@ -1709,7 +1798,7 @@
.on('keyup', $.proxy(this.keyup, this))
if ($.browser.webkit || $.browser.msie) {
this.$element.on('keydown', $.proxy(this.keypress, this))
this.$element.on('keydown', $.proxy(this.keydown, this))
}
this.$menu
@@ -1717,6 +1806,40 @@
.on('mouseenter', 'li', $.proxy(this.mouseenter, this))
}
, move: function (e) {
if (!this.shown) return
switch(e.keyCode) {
case 9: // tab
case 13: // enter
case 27: // escape
e.preventDefault()
break
case 38: // up arrow
e.preventDefault()
this.prev()
break
case 40: // down arrow
e.preventDefault()
this.next()
break
}
e.stopPropagation()
}
, keydown: function (e) {
this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27])
this.move(e)
}
, keypress: function (e) {
if (this.suppressKeyPressRepeat) return
this.move(e)
}
, keyup: function (e) {
switch(e.keyCode) {
case 40: // down arrow
@@ -1742,32 +1865,6 @@
e.preventDefault()
}
, keypress: function (e) {
if (!this.shown) return
switch(e.keyCode) {
case 9: // tab
case 13: // enter
case 27: // escape
e.preventDefault()
break
case 38: // up arrow
if (e.type != 'keydown') break
e.preventDefault()
this.prev()
break
case 40: // down arrow
if (e.type != 'keydown') break
e.preventDefault()
this.next()
break
}
e.stopPropagation()
}
, blur: function (e) {
var that = this
setTimeout(function () { that.hide() }, 150)
@@ -1805,12 +1902,13 @@
, items: 8
, menu: '<ul class="typeahead dropdown-menu"></ul>'
, item: '<li><a href="#"></a></li>'
, minLength: 1
}
$.fn.typeahead.Constructor = Typeahead
/* TYPEAHEAD DATA-API
/* TYPEAHEAD DATA-API
* ================== */
$(function () {
@@ -1822,4 +1920,108 @@
})
})
}(window.jQuery);
/* ==========================================================
* bootstrap-affix.js v2.1.0
* http://twitter.github.com/bootstrap/javascript.html#affix
* ==========================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================== */
!function ($) {
"use strict"; // jshint ;_;
/* AFFIX CLASS DEFINITION
* ====================== */
var Affix = function (element, options) {
this.options = $.extend({}, $.fn.affix.defaults, options)
this.$window = $(window).on('scroll.affix.data-api', $.proxy(this.checkPosition, this))
this.$element = $(element)
this.checkPosition()
}
Affix.prototype.checkPosition = function () {
if (!this.$element.is(':visible')) return
var scrollHeight = $(document).height()
, scrollTop = this.$window.scrollTop()
, position = this.$element.offset()
, offset = this.options.offset
, offsetBottom = offset.bottom
, offsetTop = offset.top
, reset = 'affix affix-top affix-bottom'
, affix
if (typeof offset != 'object') offsetBottom = offsetTop = offset
if (typeof offsetTop == 'function') offsetTop = offset.top()
if (typeof offsetBottom == 'function') offsetBottom = offset.bottom()
affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ?
false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ?
'bottom' : offsetTop != null && scrollTop <= offsetTop ?
'top' : false
if (this.affixed === affix) return
this.affixed = affix
this.unpin = affix == 'bottom' ? position.top - scrollTop : null
this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : ''))
}
/* AFFIX PLUGIN DEFINITION
* ======================= */
$.fn.affix = function (option) {
return this.each(function () {
var $this = $(this)
, data = $this.data('affix')
, options = typeof option == 'object' && option
if (!data) $this.data('affix', (data = new Affix(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.affix.Constructor = Affix
$.fn.affix.defaults = {
offset: 0
}
/* AFFIX DATA-API
* ============== */
$(window).on('load', function () {
$('[data-spy="affix"]').each(function () {
var $spy = $(this)
, data = $spy.data()
data.offset = data.offset || {}
data.offsetBottom && (data.offset.bottom = data.offsetBottom)
data.offsetTop && (data.offset.top = data.offsetTop)
$spy.affix(data)
})
})
}(window.jQuery);

File diff suppressed because one or more lines are too long

View File

@@ -14,6 +14,10 @@ table.data td[align=right] {
.shiny-output-error {
color: red;
}
.shiny-output-error:before {
content: 'Error: ';
font-weight: bold;
}
.jslider {
/* Fix jslider running into the control above it */

View File

@@ -4,6 +4,68 @@
var exports = window.Shiny = window.Shiny || {};
function randomId() {
return Math.floor(0x100000000 + (Math.random() * 0xF00000000)).toString(16);
}
function slice(blob, start, end) {
if (blob.slice)
return blob.slice(start, end);
if (blob.mozSlice)
return blob.mozSlice(start, end);
if (blob.webkitSlice)
return blob.webkitSlice(start, end);
throw "Blob doesn't support slice";
}
var _BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder ||
window.MozBlobBuilder || window.MSBlobBuilder;
function makeBlob(parts) {
// Browser compatibility is a mess right now. The code as written works in
// a variety of modern browsers, but sadly gives a deprecation warning
// message on the console in current versions (as of this writing) of
// Chrome.
// Safari 6.0 (8536.25) on Mac OS X 10.8.1:
// Has Blob constructor but it doesn't work with ArrayBufferView args
// Google Chrome 21.0.1180.81 on Xubuntu 12.04:
// Has Blob constructor, accepts ArrayBufferView args, accepts ArrayBuffer
// but with a deprecation warning message
// Firefox 15.0 on Xubuntu 12.04:
// Has Blob constructor, accepts both ArrayBuffer and ArrayBufferView args
// Chromium 18.0.1025.168 (Developer Build 134367 Linux) on Xubuntu 12.04:
// No Blob constructor. Has WebKitBlobBuilder.
try {
return new Blob(parts);
}
catch (e) {
var blobBuilder = new _BlobBuilder();
$.each(parts, function(i, part) {
blobBuilder.append(part);
});
return blobBuilder.getBlob();
}
}
// Takes a string expression and returns a function that takes an argument.
//
// When the function is executed, it will evaluate that expression using
// "with" on the argument value, and return the result.
function scopeExprToFunc(expr) {
var func = new Function("with (this) {return (" + expr + ");}");
return function(scope) {
return func.call(scope);
};
}
var Invoker = function(target, func) {
this.target = target;
this.func = func;
@@ -307,9 +369,23 @@
var ShinyApp = function() {
this.$socket = null;
// Cached input values
this.$inputValues = {};
// Output bindings
this.$bindings = {};
// Cached values/errors
this.$values = {};
this.$errors = {};
// Conditional bindings (show/hide element based on expression)
this.$conditionals = {};
this.$pendingMessages = [];
this.$activeRequests = {};
this.$nextRequestId = 0;
};
(function() {
@@ -318,14 +394,32 @@
if (this.$socket)
throw "Connect was already called on this application object";
$.extend(initialInput, {
// IE8 and IE9 have some limitations with data URIs
"__allowDataUriScheme": typeof WebSocket !== 'undefined'
});
this.$socket = this.createSocket();
this.$initialInput = initialInput;
$.extend(this.$inputValues, initialInput);
this.$updateConditionals();
};
this.isConnected = function() {
return !!this.$socket;
};
this.createSocket = function () {
var self = this;
var socket = new WebSocket('ws://' + window.location.host, 'shiny');
var createSocketFunc = exports.createSocket || function() {
var ws = new WebSocket('ws://' + window.location.host, 'shiny');
ws.binaryType = 'arraybuffer';
return ws;
};
var socket = createSocketFunc();
socket.onopen = function() {
socket.send(JSON.stringify({
method: 'init',
@@ -352,7 +446,87 @@
data: values
});
if (this.$socket.readyState == WebSocket.CONNECTING) {
this.$sendMsg(msg);
$.extend(this.$inputValues, values);
this.$updateConditionals();
};
// NB: Including blobs will cause IE to break!
// TODO: Make blobs work with Internet Explorer
//
// Websocket messages are normally one-way--i.e. the client passes a
// message to the server but there is no way for the server to provide
// a response to that specific message. makeRequest provides a way to
// do asynchronous RPC over websocket. Each request has a method name
// and arguments, plus optionally one or more binary blobs can be
// included as well. The request is tagged with a unique number that
// the server will use to label the corresponding response.
//
// @param method A string that tells the server what logic to run.
// @param args An array of objects that should also be passed to the
// server in JSON-ified form.
// @param onSuccess A function that will be called back if the server
// responds with success. If the server provides a value in the
// response, the function will be called with it as the only argument.
// @param onError A function that will be called back if the server
// responds with error, or if the request fails for any other reason.
// The parameter to onError will be an error object or message (format
// TBD).
// @param blobs Optionally, an array of Blob, ArrayBuffer, or string
// objects that will be made available to the server as part of the
// request. Strings will be encoded using UTF-8.
this.makeRequest = function(method, args, onSuccess, onError, blobs) {
var requestId = this.$nextRequestId;
while (this.$activeRequests[requestId]) {
requestId = (requestId + 1) % 1000000000;
}
this.$nextRequestId = requestId + 1;
this.$activeRequests[requestId] = {
onSuccess: onSuccess,
onError: onError
};
var msg = JSON.stringify({
method: method,
args: args,
tag: requestId
});
if (blobs) {
// We have binary data to transfer; form a different kind of packet.
// Start with a 4-byte signature, then for each blob, emit 4 bytes for
// the length followed by the blob. The json payload is UTF-8 encoded
// and used as the first blob.
function uint32_to_buf(val) {
var buffer = new ArrayBuffer(4);
var view = new DataView(buffer);
view.setUint32(0, val, true); // little-endian
return buffer;
}
var payload = [];
payload.push(uint32_to_buf(0x01020202)); // signature
var jsonBuf = makeBlob([msg]);
payload.push(uint32_to_buf(jsonBuf.size));
payload.push(jsonBuf);
for (var i = 0; i < blobs.length; i++) {
payload.push(uint32_to_buf(blobs[i].byteLength || blobs[i].size || 0));
payload.push(blobs[i]);
}
msg = makeBlob(payload);
}
this.$sendMsg(msg);
};
this.$sendMsg = function(msg) {
if (!this.$socket.readyState) {
this.$pendingMessages.push(msg);
}
else {
@@ -361,7 +535,11 @@
};
this.receiveError = function(name, error) {
this.$values[name] = null;
if (this.$errors[name] === error)
return;
this.$errors[name] = error;
delete this.$values[name];
var binding = this.$bindings[name];
if (binding && binding.onValueError) {
@@ -370,11 +548,12 @@
}
this.receiveOutput = function(name, value) {
var oldValue = this.$values[name];
this.$values[name] = value;
if (oldValue === value)
if (this.$values[name] === value)
return;
this.$values[name] = value;
delete this.$errors[name];
var binding = this.$bindings[name];
if (binding) {
binding.onValueChange(value);
@@ -385,7 +564,11 @@
this.dispatchMessage = function(msg) {
var msgObj = JSON.parse(msg);
if (msgObj.custom !== undefined && exports.oncustommessage) {
exports.oncustommessage(msgObj.custom);
}
if (msgObj.values) {
$(document.documentElement).removeClass('shiny-busy');
for (name in this.$bindings)
this.$bindings[name].showProgress(false);
}
@@ -395,7 +578,13 @@
for (key in msgObj.values) {
this.receiveOutput(key, msgObj.values[key]);
}
if (msgObj.console) {
for (var i = 0; i < msgObj.console.length; i++) {
console.log(msgObj.console[i]);
}
}
if (msgObj.progress) {
$(document.documentElement).addClass('shiny-busy');
for (var i = 0; i < msgObj.progress.length; i++) {
var key = msgObj.progress[i];
var binding = this.$bindings[key];
@@ -404,6 +593,20 @@
}
}
}
if (msgObj.response) {
var resp = msgObj.response;
var requestId = resp.tag;
var request = this.$activeRequests[requestId];
if (request) {
delete this.$activeRequests[requestId];
if ('value' in resp)
request.onSuccess(resp.value);
else
request.onError(resp.error);
}
};
this.$updateConditionals();
};
this.bindOutput = function(id, binding) {
@@ -412,11 +615,188 @@
if (this.$bindings[id])
throw "Duplicate binding for ID " + id;
this.$bindings[id] = binding;
if (this.$values[id] !== undefined)
binding.onValueChange(this.$values[id]);
else if (this.$errors[id] !== undefined)
binding.onValueError(this.$errors[id]);
return binding;
};
this.unbindOutput = function(id, binding) {
if (this.$bindings[id] === binding) {
delete this.$bindings[id];
return true;
}
else {
return false;
}
};
this.$updateConditionals = function() {
var scope = {input: this.$inputValues, output: this.$values};
var conditionals = $(document).find('[data-display-if]');
for (var i = 0; i < conditionals.length; i++) {
var el = $(conditionals[i]);
var condFunc = el.data('data-display-if-func');
if (!condFunc) {
var condExpr = el.attr('data-display-if');
condFunc = scopeExprToFunc(condExpr);
el.data('data-display-if-func', condFunc);
}
if (condFunc(scope)) {
el.trigger('show');
el.show(0, function() {
$(this).trigger('shown');
});
}
else {
el.trigger('hide');
el.hide(0, function() {
$(this).trigger('hidden');
});
}
}
};
}).call(ShinyApp.prototype);
// Generic driver class for doing chunk-wise asynchronous processing of a
// FileList object. Subclass/clone it and override the `on*` functions to
// make it do something useful.
var FileProcessor = function(files) {
this.files = files;
this.fileReader = new FileReader();
this.fileIndex = -1;
this.pos = 0;
// Currently need to use small chunk size because R-Websockets can't
// handle continuation frames
this.chunkSize = 4096;
this.aborted = false;
this.completed = false;
var self = this;
$(this.fileReader).on('load', function(evt) {
self.$endReadChunk();
});
// TODO: Register error/abort callbacks
this.$run();
};
(function() {
// Begin callbacks. Subclassers/cloners may override any or all of these.
this.onBegin = function(files, cont) {
setTimeout(cont, 0);
};
this.onFileBegin = function(file, cont) {
setTimeout(cont, 0);
};
this.onFileChunk = function(file, offset, blob, cont) {
setTimeout(cont, 0);
};
this.onFileEnd = function(file, cont) {
setTimeout(cont, 0);
};
this.onComplete = function() {
};
this.onAbort = function() {
};
// End callbacks
// Aborts processing, unless it's already completed
this.abort = function() {
if (this.completed || this.aborted)
return;
this.aborted = true;
this.onAbort();
};
// Returns a bound function that will call this.$run one time.
this.$getRun = function() {
var self = this;
var called = false;
return function() {
if (called)
return;
called = true;
self.$run();
};
};
// This function will be called multiple times to advance the process.
// It relies on the state of the object's fields to know what to do next.
this.$run = function() {
var self = this;
if (this.aborted || this.completed)
return;
if (this.fileIndex < 0) {
// Haven't started yet--begin
this.fileIndex = 0;
this.onBegin(this.files, this.$getRun());
return;
}
if (this.fileIndex == this.files.length) {
// Just ended
this.completed = true;
this.onComplete();
return;
}
// If we got here, then we have a file to process, or we are
// in the middle of processing a file, or have just finished
// processing a file.
var file = this.files[this.fileIndex];
if (this.pos >= file.size) {
// We've read past the end of this file--it's done
this.fileIndex++;
this.pos = 0;
this.onFileEnd(file, this.$getRun());
}
else if (this.pos == 0) {
// We're just starting with this file, need to call onFileBegin
// before we actually start reading
var called = false;
this.onFileBegin(file, function() {
if (called)
return;
called = true;
self.$beginReadChunk();
});
}
else {
// We're neither starting nor ending--just start the next chunk
this.$beginReadChunk();
}
};
// Starts asynchronous read of the current chunk of the current file
this.$beginReadChunk = function() {
var file = this.files[this.fileIndex];
var blob = slice(file, this.pos, this.pos + this.chunkSize);
this.fileReader.readAsArrayBuffer(blob);
};
// Called when a chunk has been successfully read
this.$endReadChunk = function() {
var file = this.files[this.fileIndex];
var offset = this.pos;
var data = this.fileReader.result;
this.pos = this.pos + this.chunkSize;
this.onFileChunk(file, offset, makeBlob([data]),
this.$getRun());
};
}).call(FileProcessor.prototype);
var BindingRegistry = function() {
this.bindings = [];
this.bindingNames = {};
@@ -456,7 +836,7 @@
var outputBindings = exports.outputBindings = new BindingRegistry();
var OutputBinding = function() {};
var OutputBinding = exports.OutputBinding = function() {};
(function() {
// Returns a jQuery object or element array that contains the
// descendants of scope that match this binding
@@ -467,14 +847,14 @@
};
this.onValueChange = function(el, data) {
this.clearError();
this.clearError(el);
this.renderValue(el, data);
};
this.onValueError = function(el, err) {
this.renderError(el, err);
};
this.renderError = function(el, err) {
$(el).addClass('shiny-output-error').text('ERROR: ' + err.message);
$(el).addClass('shiny-output-error').text(err.message);
};
this.clearError = function(el) {
$(el).removeClass('shiny-output-error');
@@ -506,12 +886,16 @@
return $(scope).find('.shiny-plot-output');
},
renderValue: function(el, data) {
// Load the image before emptying, to minimize flicker
var img = null;
if (data) {
img = document.createElement('img');
img.src = data;
}
$(el).empty();
if (!data)
return;
var img = document.createElement('img');
img.src = data;
$(el).append(img);
if (img)
$(el).append(img);
}
});
outputBindings.register(plotOutputBinding, 'shiny.plotOutput');
@@ -522,10 +906,23 @@
return $(scope).find('.shiny-html-output');
},
renderValue: function(el, data) {
exports.unbindAll(el);
$(el).html(data);
exports.bindAll(el);
}
});
outputBindings.register(htmlOutputBinding, 'shiny.htmlOutput');
var downloadLinkOutputBinding = new OutputBinding();
$.extend(downloadLinkOutputBinding, {
find: function(scope) {
return $(scope).find('a.shiny-download-link');
},
renderValue: function(el, data) {
$(el).attr('href', data);
}
})
outputBindings.register(downloadLinkOutputBinding, 'shiny.downloadLink');
var InputBinding = exports.InputBinding = function() {
@@ -541,8 +938,11 @@
return el['data-input-id'] || el.id;
};
// Gives the input a type in case the server needs to know it
// to deserialize the JSON correctly
this.getType = function() { return false; }
this.getValue = function(el) { throw "Not implemented"; };
this.subscribe = function(el) { };
this.subscribe = function(el, callback) { };
this.unsubscribe = function(el) { };
this.getRatePolicy = function() { return null; };
@@ -567,12 +967,11 @@
el.value = value;
},
subscribe: function(el, callback) {
var self = this;
$(el).on('keyup.textInputBinding input.textInputBinding', function(event) {
callback(self, el, true);
callback(true);
});
$(el).on('change.textInputBinding', function(event) {
callback(self, el, false);
callback(false);
});
},
unsubscribe: function(el) {
@@ -638,9 +1037,8 @@
// TODO: implement
},
subscribe: function(el, callback) {
var self = this;
$(el).on('change.inputBinding', function(event) {
callback(self, el, !$(el).data('animating'));
callback(!$(el).data('animating'));
});
},
unsubscribe: function(el) {
@@ -672,9 +1070,8 @@
$(el).val(value);
},
subscribe: function(el, callback) {
var self = this;
$(el).on('change.selectInputBinding', function(event) {
callback(self, el);
callback();
});
},
unsubscribe: function(el) {
@@ -683,7 +1080,166 @@
});
inputBindings.register(selectInputBinding, 'shiny.selectInput');
var bootstrapTabInputBinding = new InputBinding();
$.extend(bootstrapTabInputBinding, {
find: function(scope) {
return scope.find('ul.nav.nav-tabs');
},
getValue: function(el) {
var anchor = $(el).children('li.active').children('a');
if (anchor.length == 1)
return this.$getTabName(anchor);
return null;
},
setValue: function(el, value) {
var self = this;
var anchors = $(el).children('li').children('a');
anchors.each(function() {
if (self.$getTabName($(this)) === value) {
$(this).tab('show');
return false;
}
});
},
subscribe: function(el, callback) {
$(el).on('shown.bootstrapTabInputBinding', function(event) {
callback();
});
},
unsubscribe: function(el) {
$(el).off('.bootstrapTabInputBinding');
},
$getTabName: function(anchor) {
return anchor.attr('data-value') || anchor.text();
}
});
inputBindings.register(bootstrapTabInputBinding, 'shiny.bootstrapTabInput');
var FileUploader = function(shinyapp, id, files) {
this.shinyapp = shinyapp;
this.id = id;
FileProcessor.call(this, files);
};
$.extend(FileUploader.prototype, FileProcessor.prototype);
(function() {
this.makeRequest = function(method, args, onSuccess, onFailure, blobs) {
this.shinyapp.makeRequest(method, args, onSuccess, onFailure, blobs);
};
this.onBegin = function(files, cont) {
var self = this;
this.makeRequest(
'uploadInit', [],
function(response) {
self.jobId = response.jobId;
cont();
},
function(error) {
});
};
this.onFileBegin = function(file, cont) {
this.onProgress(file, 0);
this.makeRequest(
'uploadFileBegin', [this.jobId, file.name, file.type, file.size],
function(response) {
cont();
},
function(error) {
});
};
this.onFileChunk = function(file, offset, blob, cont) {
this.onProgress(file, (offset + blob.size) / file.size);
this.makeRequest(
'uploadFileChunk', [this.jobId],
function(response) {
cont();
},
function(error) {
},
[blob]);
};
this.onFileEnd = function(file, cont) {
this.makeRequest(
'uploadFileEnd', [this.jobId],
function(response) {
cont();
},
function(error) {
});
};
this.onComplete = function() {
this.makeRequest(
'uploadEnd', [this.jobId, this.id],
function(response) {
},
function(error) {
});
};
this.onAbort = function() {
};
this.onProgress = function(file, completed) {
console.log('file: ' + file.name + ' [' + Math.round(completed*100) + '%]');
};
}).call(FileUploader.prototype);
function uploadFiles(evt) {
// If previously selected files are uploading, abort that.
var el = $(evt.target);
var uploader = el.data('currentUploader');
if (uploader)
uploader.abort();
var files = evt.target.files;
var id = fileInputBinding.getId(evt.target);
// Start the new upload and put the uploader in 'currentUploader'.
el.data('currentUploader', new FileUploader(exports.shinyapp, id, files));
};
var fileInputBinding = new InputBinding();
$.extend(fileInputBinding, {
find: function(scope) {
return scope.find('input[type="file"]');
},
getId: function(el) {
return InputBinding.prototype.getId.call(this, el) || el.name;
},
getValue: function(el) {
return null;
},
setValue: function(el, value) {
// Not implemented
},
subscribe: function(el, callback) {
$(el).on('change.fileInputBinding', uploadFiles);
},
unsubscribe: function(el) {
$(el).off('.fileInputBinding');
}
})
inputBindings.register(fileInputBinding, 'shiny.fileInputBinding');
var OutputBindingAdapter = function(el, binding) {
this.el = el;
this.binding = binding;
};
(function() {
this.onValueChange = function(data) {
this.binding.onValueChange(this.el, data);
};
this.onValueError = function(err) {
this.binding.onValueError(this.el, err);
};
this.showProgress = function(show) {
this.binding.showProgress(this.el, show);
};
}).call(OutputBindingAdapter.prototype);
function initShiny() {
@@ -691,21 +1247,8 @@
function bindOutputs(scope) {
var OutputBindingAdapter = function(el, binding) {
this.el = el;
this.binding = binding;
};
$.extend(OutputBindingAdapter.prototype, {
onValueChange: function(data) {
this.binding.onValueChange(this.el, data);
},
onValueError: function(err) {
this.binding.onValueError(this.el, err);
},
showProgress: function(show) {
this.binding.showProgress(this.el, show);
}
});
if (scope == undefined)
scope = document;
scope = $(scope);
@@ -722,9 +1265,30 @@
if (!id)
continue;
shinyapp.bindOutput(id, new OutputBindingAdapter(el, binding));
var bindingAdapter = new OutputBindingAdapter(el, binding);
shinyapp.bindOutput(id, bindingAdapter);
$(el).data('shiny-output-binding', bindingAdapter);
$(el).addClass('shiny-bound-output');
}
}
// Send later in case DOM layout isn't final yet.
setTimeout(sendPlotSize, 0);
}
function unbindOutputs(scope) {
if (scope == undefined)
scope = document;
var outputs = $(scope).find('.shiny-bound-output');
for (var i = 0; i < outputs.length; i++) {
var bindingAdapter = $(outputs[i]).data('shiny-output-binding');
if (!bindingAdapter)
continue;
var id = bindingAdapter.binding.getId(outputs[i]);
shinyapp.unbindOutput(id, bindingAdapter);
$(outputs[i]).removeClass('shiny-bound-output');
}
}
function elementToValue(el) {
@@ -755,12 +1319,19 @@
function valueChangeCallback(binding, el, allowDeferred) {
var id = binding.getId(el);
var el = binding.getValue(el);
if (id)
if (id) {
var el = binding.getValue(el);
var type = binding.getType(el);
if (type)
id = id + ":" + type
inputs.setInput(id, el, !allowDeferred);
}
}
function bindInputs(scope) {
if (scope == undefined)
scope = document;
scope = $(scope);
@@ -779,8 +1350,21 @@
if (!id || boundInputs[id])
continue;
currentValues[id] = binding.getValue(el);
binding.subscribe(el, valueChangeCallback);
var type = binding.getType(el);
var effectiveId = type ? id + ":" + type : id;
currentValues[effectiveId] = binding.getValue(el);
var thisCallback = (function() {
var thisBinding = binding;
var thisEl = el;
return function(allowDeferred) {
valueChangeCallback(thisBinding, thisEl, allowDeferred);
};
})();
binding.subscribe(el, thisCallback);
$(el).data('shiny-input-binding', binding);
$(el).addClass('shiny-bound-input');
var ratePolicy = binding.getRatePolicy();
if (ratePolicy != null) {
inputsRate.setRatePolicy(
@@ -793,14 +1377,32 @@
binding: binding,
node: el
};
if (shinyapp.isConnected()) {
valueChangeCallback(binding, el, false);
}
}
}
return currentValues;
}
bindOutputs(document);
initialValues = bindInputs(document);
function unbindInputs(scope) {
if (scope == undefined)
scope = document;
var inputs = $(scope).find('.shiny-bound-input');
for (var i = 0; i < inputs.length; i++) {
var binding = $(inputs[i]).data('shiny-input-binding');
if (!binding)
continue;
var id = binding.getId(inputs[i]);
$(inputs[i]).removeClass('shiny-bound-input');
delete boundInputs[id];
binding.unsubscribe(inputs[i]);
}
}
function getMultiValue(input, exclusiveValue) {
if (!input.name)
@@ -822,7 +1424,7 @@
}
}
function configureMultiInput(selector, exclusiveValue) {
function bindMultiInput(selector, exclusiveValue) {
$(document).on('change input', selector, function() {
if (this.name) {
inputs.setInput(this.name, getMultiValue(this, exclusiveValue));
@@ -834,7 +1436,11 @@
}
}
});
$(selector).each(function() {
}
function getMultiInputValues(scope, selector, exclusiveValue) {
var initialValues = {};
$(scope).find(selector).each(function() {
if (this.name) {
initialValues[this.name] = getMultiValue(this, exclusiveValue);
}
@@ -845,10 +1451,37 @@
}
}
});
return initialValues;
}
configureMultiInput('input[type="checkbox"]', false);
configureMultiInput('input[type="radio"]', true);
function _bindAll(scope) {
bindOutputs(scope);
return $.extend(
{},
getMultiInputValues(scope, 'input[type="checkbox"]', false),
getMultiInputValues(scope, 'input[type="radio"]', true),
bindInputs(scope)
);
}
function unbindAll(scope) {
unbindInputs(scope);
unbindOutputs(scope);
}
exports.bindAll = function(scope) {
// _bindAll alone returns initial values, it doesn't send them to the
// server. export.bindAll needs to send the values to the server, so we
// wrap _bindAll in a closure that does that.
var currentValues = _bindAll(scope);
$.each(currentValues, function(name, value) {
inputs.setInput(name, value);
});
};
exports.unbindAll = unbindAll;
bindMultiInput('input[type="checkbox"]', false);
bindMultiInput('input[type="radio"]', true);
var initialValues = _bindAll(document);
// The server needs to know the size of each plot output element, in case
// the plot is auto-sizing
@@ -869,8 +1502,7 @@
// of 0x0). It's OK to over-report sizes because the input pipeline will
// filter out values that haven't changed.
$(window).resize(debounce(500, sendPlotSize));
$(window).on(
'shown', '[data-toggle="tab"], [data-toggle="pill"]', sendPlotSize);
$('body').on('shown.sendPlotSize hidden.sendPlotSize', '*', sendPlotSize);
// We've collected all the initial values--start the server process!
shinyapp.connect(initialValues);

36
man/addResourcePath.Rd Normal file
View File

@@ -0,0 +1,36 @@
\name{addResourcePath}
\alias{addResourcePath}
\title{Resource Publishing}
\usage{
addResourcePath(prefix, directoryPath)
}
\arguments{
\item{prefix}{The URL prefix (without slashes). Valid
characters are a-z, A-Z, 0-9, hyphen, and underscore; and
must begin with a-z or A-Z. For example, a value of 'foo'
means that any request paths that begin with '/foo' will
be mapped to the given directory.}
\item{directoryPath}{The directory that contains the
static resources to be served.}
}
\description{
Adds a directory of static resources to Shiny's web
server, with the given path prefix. Primarily intended
for package authors to make supporting JavaScript/CSS
files available to their components.
}
\details{
You can call \code{addResourcePath} multiple times for a
given \code{prefix}; only the most recent value will be
retained. If the normalized \code{directoryPath} is
different than the directory that's currently mapped to
the \code{prefix}, a warning will be issued.
}
\examples{
addResourcePath('datasets', system.file('data', package='datasets'))
}
\seealso{
\code{\link{singleton}}
}

26
man/bootstrapPage.Rd Normal file
View File

@@ -0,0 +1,26 @@
\name{bootstrapPage}
\alias{bootstrapPage}
\title{Create a Twitter Bootstrap page}
\usage{
bootstrapPage(...)
}
\arguments{
\item{...}{The contents of the document body.}
}
\value{
A UI defintion that can be passed to the \link{shinyUI}
function.
}
\description{
Create a Shiny UI page that loads the CSS and JavaScript
for \href{http://getbootstrap.com}{Twitter Bootstrap},
and has no content in the page body (other than what you
provide).
}
\details{
This function is primarily intended for users who are
proficient in HTML/CSS, and know how to lay out pages in
Bootstrap. Most users should use template functions like
\code{\link{pageWithSidebar}}.
}

39
man/checkboxGroupInput.Rd Normal file
View File

@@ -0,0 +1,39 @@
\name{checkboxGroupInput}
\alias{checkboxGroupInput}
\title{Checkbox Group Input Control}
\usage{
checkboxGroupInput(inputId, label, choices,
selected = NULL)
}
\arguments{
\item{inputId}{Input variable to assign the control's
value to.}
\item{label}{Display label for the control.}
\item{choices}{List of values to show checkboxes for. If
elements of the list are named then that name rather than
the value is displayed to the user.}
\item{selected}{Names of items that should be initially
selected, if any.}
}
\value{
A list of HTML elements that can be added to a UI
definition.
}
\description{
Create a group of checkboxes that can be used to toggle
multiple choices independently. The server will receive
the input as a character vector of the selected values.
}
\examples{
checkboxGroupInput("variable", "Variable:",
c("Cylinders" = "cyl",
"Transmission" = "am",
"Gears" = "gear"))
}
\seealso{
\code{\link{checkboxInput}}
}

View File

@@ -1,25 +1,29 @@
\name{checkboxInput}
\alias{checkboxInput}
\title{Create a checkbox input control}
\title{Checkbox Input Control}
\usage{
checkboxInput(inputId, label, value = FALSE)
}
\arguments{
\item{inputId}{Input variable to assign the control's
value to}
value to.}
\item{label}{Display label for the control}
\item{label}{Display label for the control.}
\item{value}{Initial value}
\item{value}{Initial value (\code{TRUE} or
\code{FALSE}).}
}
\value{
A checkbox control that can be added to a UI definition.
}
\description{
Create a checkbox that can be used to specify logical
values
values.
}
\examples{
checkboxInput("outliers", "Show outliers", FALSE)
}
\seealso{
\code{\link{checkboxGroupInput}}
}

54
man/conditionalPanel.Rd Normal file
View File

@@ -0,0 +1,54 @@
\name{conditionalPanel}
\alias{conditionalPanel}
\title{Conditional Panel}
\usage{
conditionalPanel(condition, ...)
}
\arguments{
\item{condition}{A JavaScript expression that will be
evaluated repeatedly to determine whether the panel
should be displayed.}
\item{...}{Elements to include in the panel.}
}
\description{
Creates a panel that is visible or not, depending on the
value of a JavaScript expression. The JS expression is
evaluated once at startup and whenever Shiny detects a
relevant change in input/output.
}
\details{
In the JS expression, you can refer to \code{input} and
\code{output} JavaScript objects that contain the current
values of input and output. For example, if you have an
input with an id of \code{foo}, then you can use
\code{input.foo} to read its value. (Be sure not to
modify the input/output objects, as this may cause
unpredictable behavior.)
}
\examples{
sidebarPanel(
selectInput(
"plotType", "Plot Type",
c(Scatter = "scatter",
Histogram = "hist")),
# Only show this panel if the plot type is a histogram
conditionalPanel(
condition = "input.plotType == 'hist'",
selectInput(
"breaks", "Breaks",
c("Sturges",
"Scott",
"Freedman-Diaconis",
"[Custom]" = "custom")),
# Only show this panel if Custom is selected
conditionalPanel(
condition = "input.breaks == 'custom'",
sliderInput("breakCount", "Break Count", min=1, max=1000, value=10)
)
)
)
}

46
man/downloadButton.Rd Normal file
View File

@@ -0,0 +1,46 @@
\name{downloadButton}
\alias{downloadButton}
\alias{downloadLink}
\title{Create a download button or link}
\usage{
downloadButton(outputId, label = "Download",
class = NULL)
downloadLink(outputId, label = "Download", class = NULL)
}
\arguments{
\item{outputId}{The name of the output slot that the
\code{downloadHandler} is assigned to.}
\item{label}{The label that should appear on the button.}
\item{class}{Additional CSS classes to apply to the tag,
if any.}
}
\description{
Use these functions to create a download button or link;
when clicked, it will initiate a browser download. The
filename and contents are specified by the corresponding
\code{\link{downloadHandler}} defined in the server
function.
}
\examples{
\dontrun{
# In server.R:
output$downloadData <- downloadHandler(
filename = function() {
paste('data-', Sys.Date(), '.csv', sep='')
},
content = function(con) {
write.csv(data, con)
}
)
# In ui.R:
downloadLink('downloadData', 'Download')
}
}
\seealso{
downloadHandler
}

55
man/downloadHandler.Rd Normal file
View File

@@ -0,0 +1,55 @@
\name{downloadHandler}
\alias{downloadHandler}
\title{File Downloads}
\usage{
downloadHandler(filename, content, contentType = NA)
}
\arguments{
\item{filename}{A string of the filename, including
extension, that the user's web browser should default to
when downloading the file; or a function that returns
such a string. (Reactive values and functions may be used
from this function.)}
\item{content}{A function that takes a single argument
\code{file} that is a file path (string) of a nonexistent
temp file, and writes the content to that file path.
(Reactive values and functions may be used from this
function.)}
\item{contentType}{A string of the download's
\href{http://en.wikipedia.org/wiki/Internet_media_type}{content
type}, for example \code{"text/csv"} or
\code{"image/png"}. If \code{NULL} or \code{NA}, the
content type will be guessed based on the filename
extension, or \code{application/octet-stream} if the
extension is unknown.}
}
\description{
Allows content from the Shiny application to be made
available to the user as file downloads (for example,
downloading the currently visible data as a CSV file).
Both filename and contents can be calculated dynamically
at the time the user initiates the download. Assign the
return value to a slot on \code{output} in your server
function, and in the UI use \code{\link{downloadButton}}
or \code{\link{downloadLink}} to make the download
available.
}
\examples{
\dontrun{
# In server.R:
output$downloadData <- downloadHandler(
filename = function() {
paste('data-', Sys.Date(), '.csv', sep='')
},
content = function(file) {
write.csv(data, file)
}
)
# In ui.R:
downloadLink('downloadData', 'Download')
}
}

27
man/fileInput.Rd Normal file
View File

@@ -0,0 +1,27 @@
\name{fileInput}
\alias{fileInput}
\title{File Upload Control}
\usage{
fileInput(inputId, label, multiple = FALSE,
accept = NULL)
}
\arguments{
\item{inputId}{Input variable to assign the control's
value to.}
\item{label}{Display label for the control.}
\item{multiple}{Whether the user should be allowed to
select and upload multiple files at once.}
\item{accept}{A character vector of MIME types; gives the
browser a hint of what kind of files the server is
expecting.}
}
\description{
Create a file upload control that can be used to upload
one or more files. \bold{Experimental feature. Only works
in some browsers (primarily tested on Chrome and
Firefox).}
}

View File

@@ -2,10 +2,14 @@
\alias{headerPanel}
\title{Create a header panel}
\usage{
headerPanel(title)
headerPanel(title, windowTitle = title)
}
\arguments{
\item{title}{An application title to display}
\item{windowTitle}{The title that should be displayed by
the browser window. Useful if \code{title} is not a
string.}
}
\value{
A headerPanel that can be passed to

View File

@@ -2,12 +2,11 @@
\alias{helpText}
\title{Create a help text element}
\usage{
helpText(text, ...)
helpText(...)
}
\arguments{
\item{text}{Help text string}
\item{...}{Additional help text strings}
\item{...}{One or more help text strings (or other inline
HTML elements)}
}
\value{
A help text element that can be added to a UI definition.

View File

@@ -1,8 +1,11 @@
\name{htmlOutput}
\alias{htmlOutput}
\alias{uiOutput}
\title{Create an HTML output element}
\usage{
htmlOutput(outputId)
uiOutput(outputId)
}
\arguments{
\item{outputId}{output variable to read the value from}
@@ -16,6 +19,11 @@
HTML \code{div} tag, and is presumed to contain HTML
content which should not be escaped.
}
\details{
\code{uiOutput} is intended to be used with
\code{reactiveUI} on the server side. It is currently
just an alias for \code{htmlOutput}.
}
\examples{
htmlOutput("summary")
}

54
man/include.Rd Normal file
View File

@@ -0,0 +1,54 @@
\name{includeHTML}
\alias{includeHTML}
\alias{includeText}
\alias{includeMarkdown}
\usage{
includeHTML(path)
includeText(path)
includeMarkdown(path)
}
\title{Include Content From a File}
\arguments{
\item{path}{
The path of the file to be included. It is highly recommended to
use a relative path (the base path being the Shiny application
directory), not an absolute path.
}
}
\description{
Include HTML, text, or rendered Markdown into a \link[=shinyUI]{Shiny UI}.
}
\details{
These functions provide a convenient way to include an extensive amount
of HTML, textual, or Markdown content, rather than using a large literal R
string.
}
\note{
\code{includeText} escapes its contents, but does no other processing. This
means that hard breaks and multiple spaces will be rendered as they usually
are in HTML: as a single space character. If you are looking for
preformatted text, wrap the call with \code{\link{pre}}, or consider using
\code{includeMarkdown} instead.
}
\note{
The \code{includeMarkdown} function requires the \code{markdown} package.
}
\examples{
doc <- tags$html(
tags$head(
tags$title('My first page')
),
tags$body(
h1('My first heading'),
p('My first paragraph, with some ',
strong('bold'),
' text.'),
div(id='myDiv', class='simpleDiv',
'Here is a div with some attributes.')
)
)
cat(as.character(doc))
}

33
man/observe.Rd Normal file
View File

@@ -0,0 +1,33 @@
\name{observe}
\alias{observe}
\title{Create a reactive observer}
\usage{
observe(func)
}
\arguments{
\item{func}{The function to observe. It must not have any
parameters. Any return value from this function will be
ignored.}
}
\description{
Creates an observer from the given function. An observer
is like a reactive function in that it can read reactive
values and call reactive functions, and will
automatically re-execute when those dependencies change.
But unlike reactive functions, it doesn't yield a result
and can't be used as an input to other reactive
functions. Thus, observers are only useful for their side
effects (for example, performing I/O).
}
\details{
Another contrast between reactive functions and observers
is their execution strategy. Reactive functions use lazy
evaluation; that is, when their dependencies change, they
don't re-execute right away but rather wait until they
are called by someone else. Indeed, if they are not
called then they will never re-execute. In contrast,
observers use eager evaluation; as soon as their
dependencies change, they schedule themselves to
re-execute.
}

View File

@@ -27,9 +27,9 @@
}
\examples{
radioButtons("dist", "Distribution type:",
list("Normal" = "norm",
"Uniform" = "unif",
"Log-normal" = "lnorm",
"Exponential" = "exp"))
c("Normal" = "norm",
"Uniform" = "unif",
"Log-normal" = "lnorm",
"Exponential" = "exp"))
}

View File

@@ -9,11 +9,17 @@
\item{width}{The width of the rendered plot, in pixels;
or \code{'auto'} to use the \code{offsetWidth} of the
HTML element that is bound to this plot.}
HTML element that is bound to this plot. You can also
pass in a function that returns the width in pixels or
\code{'auto'}; in the body of the function you may
reference reactive values and functions.}
\item{height}{The height of the rendered plot, in pixels;
or \code{'auto'} to use the \code{offsetHeight} of the
HTML element that is bound to this plot.}
HTML element that is bound to this plot. You can also
pass in a function that returns the width in pixels or
\code{'auto'}; in the body of the function you may
reference reactive values and functions.}
\item{...}{Arguments to be passed through to
\code{\link[grDevices]{png}}. These can be used to set

View File

@@ -9,7 +9,8 @@
be used with \code{\link[xtable]{xtable}}.}
\item{...}{Arguments to be passed through to
\code{\link[xtable]{xtable}}.}
\code{\link[xtable]{xtable}} and
\code{\link[xtable]{print.xtable}}.}
}
\description{
Creates a reactive table that is suitable for assigning

33
man/reactiveUI.Rd Normal file
View File

@@ -0,0 +1,33 @@
\name{reactiveUI}
\alias{reactiveUI}
\title{UI Output}
\usage{
reactiveUI(func)
}
\arguments{
\item{func}{A function that returns a Shiny tag object,
\code{\link{HTML}}, or a list of such objects.}
}
\description{
\bold{Experimental feature.} Makes a reactive version of
a function that generates HTML using the Shiny UI
library.
}
\details{
The corresponding HTML output tag should be \code{div}
and have the CSS class name \code{shiny-html-output} (or
use \code{\link{uiOutput}}).
}
\examples{
\dontrun{
output$moreControls <- reactiveUI(function() {
list(
)
})
}
}
\seealso{
conditionalPanel
}

38
man/repeatable.Rd Normal file
View File

@@ -0,0 +1,38 @@
\name{repeatable}
\alias{repeatable}
\title{Make a random number generator repeatable}
\usage{
repeatable(rngfunc,
seed = runif(1, 0, .Machine$integer.max))
}
\arguments{
\item{rngfunc}{The function that is affected by the R
session's seed.}
\item{seed}{The seed to set every time the resulting
function is called.}
}
\value{
A repeatable version of the function that was passed in.
}
\description{
Given a function that generates random data, returns a
wrapped version of that function that always uses the
same seed when called. The seed to use can be passed in
explicitly if desired; otherwise, a random number is
used.
}
\note{
When called, the returned function attempts to preserve
the R session's current seed by snapshotting and
restoring \code{\link[base]{.Random.seed}}.
}
\examples{
rnormA <- repeatable(rnorm)
rnormB <- repeatable(rnorm)
rnormA(3) # [1] 1.8285879 -0.7468041 -0.4639111
rnormA(3) # [1] 1.8285879 -0.7468041 -0.4639111
rnormA(5) # [1] 1.8285879 -0.7468041 -0.4639111 -1.6510126 -1.4686924
rnormB(5) # [1] -0.7946034 0.2568374 -0.6567597 1.2451387 -0.8375699
}

26
man/runGist.Rd Normal file
View File

@@ -0,0 +1,26 @@
\name{runGist}
\alias{runGist}
\title{Run a Shiny application from https://gist.github.com}
\usage{
runGist(gist, port = 8100L,
launch.browser = getOption("shiny.launch.browser", interactive()))
}
\arguments{
\item{gist}{The identifier of the gist. For example, if
the gist is https://gist.github.com/3239667, then
\code{3239667}, \code{'3239667'}, and
\code{'https://gist.github.com/3239667'} are all valid
values.}
\item{port}{The TCP port that the application should
listen on. Defaults to port 8100.}
\item{launch.browser}{If true, the system's default web
browser will be launched automatically after the app is
started. Defaults to true in interactive sessions only.}
}
\description{
Download and launch a Shiny application that is hosted on
GitHub as a gist.
}

View File

@@ -33,8 +33,8 @@
}
\examples{
selectInput("variable", "Variable:",
list("Cylinders" = "cyl",
"Transmission" = "am",
"Gears" = "gear"))
c("Cylinders" = "cyl",
"Transmission" = "am",
"Gears" = "gear"))
}

View File

@@ -2,19 +2,24 @@
\alias{tabPanel}
\title{Create a tab panel}
\usage{
tabPanel(title, ...)
tabPanel(title, ..., value = NULL)
}
\arguments{
\item{title}{Display title for tab}
\item{...}{UI elements to include within the tab}
\item{value}{The value that should be sent when
\code{tabsetPanel} reports that this tab is selected. If
omitted and \code{tabsetPanel} has an \code{id}, then the
title will be used.}
}
\value{
A tab that can be passed to \link{tabsetPanel}
A tab that can be passed to \code{\link{tabsetPanel}}
}
\description{
Create a tab panel that can be inluded within a
\link{tabsetPanel}.
Create a tab panel that can be included within a
\code{\link{tabsetPanel}}.
}
\examples{
# Show a tabset that includes a plot, summary, and

View File

@@ -2,19 +2,25 @@
\alias{tabsetPanel}
\title{Create a tabset panel}
\usage{
tabsetPanel(...)
tabsetPanel(..., id = NULL)
}
\arguments{
\item{...}{\link{tabPanel} elements to include in the
tabset}
\item{...}{\code{\link{tabPanel}} elements to include in
the tabset}
\item{id}{If provided, you can use
\code{input$}\emph{\code{id}} in your server logic to
determine which of the current tabs is active. The value
will correspond to the \code{value} argument that is
passed to \code{\link{tabPanel}}.}
}
\value{
A tabset that can be passed to \link{mainPanel}
A tabset that can be passed to \code{\link{mainPanel}}
}
\description{
Create a tabset that contains \link{tabPanel} elements.
Tabsets are useful for dividing output into multiple
independently viewable sections.
Create a tabset that contains \code{\link{tabPanel}}
elements. Tabsets are useful for dividing output into
multiple independently viewable sections.
}
\examples{
# Show a tabset that includes a plot, summary, and

View File

@@ -1,15 +1,22 @@
\name{tag}
\alias{tag}
\alias{tagAppendChild}
\alias{tagList}
\title{
HTML Tag Object
}
\description{
Create an HTML tag definition. Note that all of the valid HTML5 tags are already defined in the \link{tags} environment so these functions should only be used to generate additional tags.
\code{tag} creates an HTML tag definition. Note that all of the valid HTML5 tags
are already defined in the \link{tags} environment so these functions should
only be used to generate additional tags. \code{tagAppendChild} and
\code{tagList} are for supporting package authors who wish to create their own
sets of tags; see the contents of bootstrap.R for examples.
\code{tag(_tag_name, varArgs)}
\code{tagAppendChild(tag, child)}
\code{tagList(...)}
}
\arguments{
@@ -18,7 +25,7 @@ Create an HTML tag definition. Note that all of the valid HTML5 tags are already
}
\item{varArgs}{
List of attributes and children of the element. Named list items
become attributes, and other items become children. Valid
become attributes, and unnamed list items become children. Valid
children are tags, single-character character vectors (which become
text nodes), and raw HTML (see \code{\link{HTML}}). You can also
pass lists that contain tags, text nodes, and HTML.
@@ -28,6 +35,9 @@ Create an HTML tag definition. Note that all of the valid HTML5 tags are already
}
\item{child}{
A child element to append to a parent tag.
}
\item{...}{
Unnamed items that comprise this list of tags.
}
}

18
man/wellPanel.Rd Normal file
View File

@@ -0,0 +1,18 @@
\name{wellPanel}
\alias{wellPanel}
\title{Create a well panel}
\usage{
wellPanel(...)
}
\arguments{
\item{...}{UI elements to include inside the panel.}
}
\value{
The newly created panel.
}
\description{
Creates a panel with a slightly inset border and grey
background. Equivalent to Twitter Bootstrap's \code{well}
CSS class.
}