[med-svn] [r-cran-shiny] 01/07: New upstream version 0.14.2+dfsg

Andreas Tille tille at debian.org
Wed Nov 2 15:51:54 UTC 2016


This is an automated email from the git hooks/post-receive script.

tille pushed a commit to branch master
in repository r-cran-shiny.

commit 071cde2375d6843cca7abf62b0ad2afadb9e6746
Author: Andreas Tille <tille at debian.org>
Date:   Wed Nov 2 16:33:27 2016 +0100

    New upstream version 0.14.2+dfsg
---
 DESCRIPTION                                        |   36 +-
 MD5                                                |  564 +-
 NAMESPACE                                          |   32 +
 NEWS                                               |  970 ---
 NEWS.md                                            | 1032 +++
 R/app.R                                            |   41 +-
 R/bookmark-state-local.R                           |   28 +
 R/bookmark-state.R                                 | 1113 +++
 R/bootstrap-layout.R                               |  147 +-
 R/bootstrap.R                                      |  353 +-
 R/conditions.R                                     |   11 +-
 R/diagnose.R                                       |  157 +
 R/fileupload.R                                     |    2 +-
 R/graph.R                                          |    9 +-
 R/html-deps.R                                      |   20 +-
 R/imageutils.R                                     |    1 -
 R/input-action.R                                   |   67 +-
 R/input-checkbox.R                                 |   16 +-
 R/input-checkboxgroup.R                            |   24 +-
 R/input-date.R                                     |   97 +-
 R/input-daterange.R                                |   61 +-
 R/input-file.R                                     |   76 +-
 R/input-numeric.R                                  |   17 +-
 R/input-password.R                                 |   23 +-
 R/input-radiobuttons.R                             |   34 +-
 R/input-select.R                                   |   28 +-
 R/input-slider.R                                   |   33 +-
 R/input-text.R                                     |   15 +-
 R/input-textarea.R                                 |   69 +
 R/input-utils.R                                    |   13 +-
 R/insert-ui.R                                      |  174 +
 R/jqueryui.R                                       |    3 -
 R/middleware.R                                     |   19 +-
 R/modal.R                                          |  183 +
 R/modules.R                                        |   19 +-
 R/notifications.R                                  |  106 +
 R/progress.R                                       |   88 +-
 R/react.R                                          |    5 +-
 R/reactive-domains.R                               |    6 +-
 R/reactives.R                                      |  297 +-
 R/render-plot.R                                    |  409 +-
 R/render-table.R                                   |  226 +-
 R/run-url.R                                        |    2 +-
 R/serializers.R                                    |   72 +
 R/server-input-handlers.R                          |  113 +-
 R/server.R                                         |  272 +-
 R/shiny-options.R                                  |   83 +
 R/shiny.R                                          |  876 +-
 R/shinyui.R                                        |   41 +-
 R/shinywrappers.R                                  |  246 +-
 R/showcase.R                                       |   87 +-
 R/test-export.R                                    |   60 +
 R/timer.R                                          |   20 +-
 R/update-input.R                                   |  385 +-
 R/utils.R                                          |  358 +-
 README.md                                          |    4 +-
 build/vignette.rds                                 |  Bin 214 -> 0 bytes
 inst/doc/events.R                                  |   20 -
 inst/doc/events.Rmd                                |  121 -
 inst/doc/events.html                               |  281 -
 .../rsconnect/localhost/admin/01_hello.dcf         |    6 -
 inst/examples/01_hello/server.R                    |    4 +-
 inst/examples/01_hello/ui.R                        |    4 +-
 inst/examples/02_text/server.R                     |    4 +-
 inst/examples/02_text/ui.R                         |    4 +-
 inst/examples/03_reactivity/server.R               |    4 +-
 inst/examples/03_reactivity/ui.R                   |    4 +-
 inst/examples/04_mpg/server.R                      |    4 +-
 inst/examples/04_mpg/ui.R                          |    4 +-
 inst/examples/05_sliders/server.R                  |    4 +-
 inst/examples/05_sliders/ui.R                      |    4 +-
 inst/examples/06_tabsets/server.R                  |    4 +-
 inst/examples/06_tabsets/ui.R                      |    4 +-
 inst/examples/07_widgets/server.R                  |    4 +-
 inst/examples/07_widgets/ui.R                      |    4 +-
 inst/examples/08_html/server.R                     |    4 +-
 inst/examples/09_upload/server.R                   |    4 +-
 inst/examples/09_upload/ui.R                       |    4 +-
 inst/examples/10_download/server.R                 |    4 +-
 inst/examples/10_download/ui.R                     |    4 +-
 inst/examples/11_timer/server.R                    |    4 +-
 inst/examples/11_timer/ui.R                        |    4 +-
 inst/staticdocs/index.r                            |   34 +-
 inst/www/reactive-graph.html                       | 1072 ++-
 inst/www/shared/babel-polyfill.min.js              |    3 +
 inst/www/shared/highlight/rstudio.css              |   56 +-
 .../shared/ionrangeslider/js/ion.rangeSlider.js    |  857 +-
 .../ionrangeslider/js/ion.rangeSlider.min.js       |    4 +-
 inst/www/shared/jquery-AUTHORS.txt                 |   51 +-
 inst/www/shared/jqueryui/AUTHORS.txt               |  333 +
 inst/www/shared/jqueryui/LICENSE.txt               |   43 +
 inst/www/shared/jqueryui/README                    |    7 +-
 inst/www/shared/jqueryui/index.html                |  112 +-
 inst/www/shared/jqueryui/jquery-ui.structure.css   |  730 +-
 .../shared/jqueryui/jquery-ui.structure.min.css    |    4 +-
 inst/www/shared/jqueryui/jquery-ui.theme.css       |  101 +-
 inst/www/shared/jqueryui/jquery-ui.theme.min.css   |    4 +-
 inst/www/shared/shiny-showcase.js                  |   20 +-
 inst/www/shared/shiny.css                          |  142 +-
 inst/www/shared/shiny.js                           | 8944 ++++++++++----------
 inst/www/shared/shiny.js.map                       |    2 +-
 inst/www/shared/shiny.min.js                       |    8 +-
 inst/www/shared/shiny.min.js.map                   |    2 +-
 man/Progress.Rd                                    |   30 +-
 man/actionButton.Rd                                |   38 +-
 man/addResourcePath.Rd                             |    1 -
 man/applyInputHandlers.Rd                          |   30 +
 man/bookmarkButton.Rd                              |   73 +
 man/checkboxGroupInput.Rd                          |   24 +-
 man/checkboxInput.Rd                               |   16 +-
 man/column.Rd                                      |   45 +-
 man/conditionalPanel.Rd                            |    1 -
 man/createWebDependency.Rd                         |    2 +-
 man/dateInput.Rd                                   |   56 +-
 man/dateRangeInput.Rd                              |   87 +-
 man/downloadHandler.Rd                             |   36 +-
 man/enableBookmarking.Rd                           |  231 +
 man/exportTestValues.Rd                            |   70 +
 man/exprToFunction.Rd                              |    1 -
 man/fileInput.Rd                                   |   44 +-
 man/fillPage.Rd                                    |    1 -
 man/fillRow.Rd                                     |    4 -
 man/fixedPage.Rd                                   |   10 +-
 man/flowLayout.Rd                                  |    7 +-
 man/fluidPage.Rd                                   |   26 +-
 man/freezeReactiveValue.Rd                         |   61 +
 man/icon.Rd                                        |    5 +-
 man/insertUI.Rd                                    |   89 +
 man/invalidateLater.Rd                             |   16 +-
 man/isolate.Rd                                     |    1 -
 man/markRenderFunction.Rd                          |    9 +-
 man/modalButton.Rd                                 |   21 +
 man/modalDialog.Rd                                 |  132 +
 man/navbarPage.Rd                                  |   35 +-
 man/navlistPanel.Rd                                |    4 +-
 man/numericInput.Rd                                |   17 +-
 man/observe.Rd                                     |   16 +-
 man/observeEvent.Rd                                |    1 -
 man/onBookmark.Rd                                  |  209 +
 man/onFlush.Rd                                     |   37 +
 man/pageWithSidebar.Rd                             |    5 +-
 man/parseQueryString.Rd                            |    4 +-
 man/passwordInput.Rd                               |   27 +-
 man/radioButtons.Rd                                |   35 +-
 man/reactive.Rd                                    |    1 -
 man/reactiveFileReader.Rd                          |    5 +-
 man/reactivePoll.Rd                                |    5 +-
 man/reactiveTimer.Rd                               |   16 +-
 man/reactiveValuesToList.Rd                        |    1 -
 man/removeUI.Rd                                    |   67 +
 man/renderDataTable.Rd                             |    6 +-
 man/renderImage.Rd                                 |   30 +-
 man/renderPlot.Rd                                  |   15 +-
 man/renderPrint.Rd                                 |   14 +-
 man/renderTable.Rd                                 |   59 +-
 man/renderText.Rd                                  |    8 +-
 man/renderUI.Rd                                    |   24 +-
 man/repeatable.Rd                                  |    1 -
 man/req.Rd                                         |   59 +-
 man/restoreInput.Rd                                |   18 +
 man/runGadget.Rd                                   |    1 -
 man/runUrl.Rd                                      |    2 +-
 man/safeError.Rd                                   |   82 +
 man/selectInput.Rd                                 |   26 +-
 man/session.Rd                                     |   66 +-
 man/setBookmarkExclude.Rd                          |   29 +
 man/shiny-options.Rd                               |   24 +-
 man/shinyApp.Rd                                    |   10 +-
 man/shinyOptions.Rd                                |   39 +
 man/shinyServer.Rd                                 |    1 -
 man/showBookmarkUrlModal.Rd                        |   19 +
 man/showModal.Rd                                   |   25 +
 man/showNotification.Rd                            |   91 +
 man/showReactLog.Rd                                |    6 +-
 man/sidebarLayout.Rd                               |   17 +-
 man/sliderInput.Rd                                 |   25 +-
 man/splitLayout.Rd                                 |   20 +-
 man/submitButton.Rd                                |    3 +-
 man/tabsetPanel.Rd                                 |    8 +-
 man/textAreaInput.Rd                               |   73 +
 man/textInput.Rd                                   |   16 +-
 man/titlePanel.Rd                                  |    8 +-
 man/updateActionButton.Rd                          |   81 +
 man/updateCheckboxGroupInput.Rd                    |   41 +-
 man/updateCheckboxInput.Rd                         |   21 +-
 man/updateDateInput.Rd                             |   32 +-
 man/updateDateRangeInput.Rd                        |   34 +-
 man/updateNumericInput.Rd                          |   21 +-
 man/updateQueryString.Rd                           |   22 +
 man/updateRadioButtons.Rd                          |   41 +-
 man/updateSelectInput.Rd                           |   50 +-
 man/updateSliderInput.Rd                           |    3 +-
 man/updateTabsetPanel.Rd                           |   39 +-
 man/{updateTextInput.Rd => updateTextAreaInput.Rd} |   34 +-
 man/updateTextInput.Rd                             |   18 +-
 man/urlModal.Rd                                    |   22 +
 man/validate.Rd                                    |   13 +-
 man/verticalLayout.Rd                              |    9 +-
 man/withProgress.Rd                                |   31 +-
 tests/test-all.R                                   |    2 +-
 tests/testthat/helper.R                            |   50 +
 tests/testthat/test-bookmarking.R                  |   54 +
 {inst/tests => tests/testthat}/test-bootstrap.r    |   19 +-
 tests/testthat/test-diagnostics.R                  |   36 +
 {inst/tests => tests/testthat}/test-gc.r           |    0
 .../tests => tests/testthat}/test-input-handler.R  |    0
 {inst/tests => tests/testthat}/test-modules.R      |    0
 tests/testthat/test-options.R                      |   53 +
 .../tests => tests/testthat}/test-plot-coordmap.R  |  142 +-
 {inst/tests => tests/testthat}/test-reactivity.r   |   60 +
 {inst/tests => tests/testthat}/test-stack.R        |    0
 {inst/tests => tests/testthat}/test-stacks.R       |   39 +-
 {inst/tests => tests/testthat}/test-staticdocs.R   |   27 +-
 {inst/tests => tests/testthat}/test-stop-app.R     |    0
 {inst/tests => tests/testthat}/test-text.R         |    0
 tests/testthat/test-timer.R                        |   25 +
 {inst/tests => tests/testthat}/test-ui.R           |    0
 tests/testthat/test-update-input.R                 |   37 +
 {inst/tests => tests/testthat}/test-url.R          |    2 +
 {inst/tests => tests/testthat}/test-utils.R        |    0
 vignettes/events.Rmd                               |  121 -
 221 files changed, 17634 insertions(+), 8306 deletions(-)

diff --git a/DESCRIPTION b/DESCRIPTION
index de5bd8e..4a205f7 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,8 +1,7 @@
 Package: shiny
 Type: Package
 Title: Web Application Framework for R
-Version: 0.13.2
-Date: 2016-03-28
+Version: 0.14.2
 Authors at R: c(
     person("Winston", "Chang", role = c("aut", "cre"), email = "winston at rstudio.com"),
     person("Joe", "Cheng", role = "aut", email = "joe at rstudio.com"),
@@ -15,7 +14,7 @@ Authors at R: c(
     person(family = "jQuery contributors", role = c("ctb", "cph"),
     comment = "jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt"),
     person(family = "jQuery UI contributors", role = c("ctb", "cph"),
-    comment = "jQuery UI library; authors listed in inst/www/shared/jqueryui/1.10.4/AUTHORS.txt"),
+    comment = "jQuery UI library; authors listed in inst/www/shared/jqueryui/AUTHORS.txt"),
     person("Mark", "Otto", role = "ctb",
     comment = "Bootstrap library"),
     person("Jacob", "Thornton", role = "ctb",
@@ -62,30 +61,31 @@ Description: Makes it incredibly easy to build interactive web
 License: GPL-3 | file LICENSE
 Depends: R (>= 3.0.0), methods
 Imports: utils, httpuv (>= 1.3.3), mime (>= 0.3), jsonlite (>= 0.9.16),
-        xtable, digest, htmltools (>= 0.3), R6 (>= 2.0)
+        xtable, digest, htmltools (>= 0.3.5), R6 (>= 2.0), sourcetools
 Suggests: datasets, Cairo (>= 1.5-5), testthat, knitr (>= 1.6),
         markdown, rmarkdown, ggplot2
 URL: http://shiny.rstudio.com
 BugReports: https://github.com/rstudio/shiny/issues
-VignetteBuilder: knitr
-Collate: 'app.R' 'bootstrap-layout.R' 'conditions.R' 'map.R'
-        'globals.R' 'utils.R' 'bootstrap.R' 'cache.R' 'fileupload.R'
-        'stack.R' 'graph.R' 'hooks.R' 'html-deps.R' 'htmltools.R'
+Collate: 'app.R' 'bookmark-state-local.R' 'stack.R' 'bookmark-state.R'
+        'bootstrap-layout.R' 'conditions.R' 'map.R' 'globals.R'
+        'utils.R' 'bootstrap.R' 'cache.R' 'diagnose.R' 'fileupload.R'
+        'graph.R' 'hooks.R' 'html-deps.R' 'htmltools.R'
         'image-interact-opts.R' 'image-interact.R' 'imageutils.R'
         'input-action.R' 'input-checkbox.R' 'input-checkboxgroup.R'
         'input-date.R' 'input-daterange.R' 'input-file.R'
         'input-numeric.R' 'input-password.R' 'input-radiobuttons.R'
         'input-select.R' 'input-slider.R' 'input-submit.R'
-        'input-text.R' 'input-utils.R' 'jqueryui.R'
-        'middleware-shiny.R' 'middleware.R' 'modules.R'
-        'priorityqueue.R' 'progress.R' 'react.R' 'reactive-domains.R'
-        'reactives.R' 'render-plot.R' 'render-table.R' 'run-url.R'
-        'server-input-handlers.R' 'server.R' 'shiny.R' 'shinyui.R'
-        'shinywrappers.R' 'showcase.R' 'tar.R' 'timer.R'
-        'update-input.R'
+        'input-text.R' 'input-textarea.R' 'input-utils.R' 'insert-ui.R'
+        'jqueryui.R' 'middleware-shiny.R' 'middleware.R' 'modal.R'
+        'modules.R' 'notifications.R' 'priorityqueue.R' 'progress.R'
+        'react.R' 'reactive-domains.R' 'reactives.R' 'render-plot.R'
+        'render-table.R' 'run-url.R' 'serializers.R'
+        'server-input-handlers.R' 'server.R' 'shiny-options.R'
+        'shiny.R' 'shinyui.R' 'shinywrappers.R' 'showcase.R' 'tar.R'
+        'test-export.R' 'timer.R' 'update-input.R'
 RoxygenNote: 5.0.1
 NeedsCompilation: no
-Packaged: 2016-03-28 15:21:34 UTC; winston
+Packaged: 2016-10-31 18:49:17 UTC; winston
 Author: Winston Chang [aut, cre],
   Joe Cheng [aut],
   JJ Allaire [aut],
@@ -96,7 +96,7 @@ Author: Winston Chang [aut, cre],
   jQuery contributors [ctb, cph] (jQuery library; authors listed in
     inst/www/shared/jquery-AUTHORS.txt),
   jQuery UI contributors [ctb, cph] (jQuery UI library; authors listed in
-    inst/www/shared/jqueryui/1.10.4/AUTHORS.txt),
+    inst/www/shared/jqueryui/AUTHORS.txt),
   Mark Otto [ctb] (Bootstrap library),
   Jacob Thornton [ctb] (Bootstrap library),
   Bootstrap contributors [ctb] (Bootstrap library),
@@ -118,4 +118,4 @@ Author: Winston Chang [aut, cre],
   R Core Team [ctb, cph] (tar implementation from R)
 Maintainer: Winston Chang <winston at rstudio.com>
 Repository: CRAN
-Date/Publication: 2016-03-28 18:59:41
+Date/Publication: 2016-11-01 19:53:48
diff --git a/MD5 b/MD5
index 9a68f4b..804b3d8 100644
--- a/MD5
+++ b/MD5
@@ -1,140 +1,134 @@
-d6822b2e7d2aac40a6f3f9b92e61c322 *DESCRIPTION
+823fbe7a187e2e0824d280f6da81c479 *DESCRIPTION
 3ea3bb190b681ed860efbb72545a0de7 *LICENSE
-e2b2a23520dd2ecf5e564c8b06f3af85 *NAMESPACE
-628bf8ee3bac447816fce91aff6ec1b3 *NEWS
-5ab3e1e0ff631d4daba310c6a976433f *R/app.R
-b029d63dc362e63fd99f1ccc683e386d *R/bootstrap-layout.R
-75d9b77e9eb740096f1d736ac437881e *R/bootstrap.R
+b1f4cff3b8aef8d7095a6333427b08a4 *NAMESPACE
+9098adc2c34df34bb7097920e95d49c4 *NEWS.md
+e1025bd539fbcf0c49f528cfacf8a039 *R/app.R
+bc5f3a814689aee175d6472b08d45cff *R/bookmark-state-local.R
+126fca9813a088ddac9fa080e26b787c *R/bookmark-state.R
+5e8f345254f57d457071cd822e55018f *R/bootstrap-layout.R
+7bc99d26ae38f43a4323369659505fdc *R/bootstrap.R
 d18246ad3e9786fc0a4e7485cbb63eca *R/cache.R
-d3d888836ed2e5f4c138de7648369045 *R/conditions.R
-7735225d08ac11058d79c22653acb79c *R/fileupload.R
+c930e3553b07208d57cfa4240f2c4183 *R/conditions.R
+776e0c0c4e11677b365eeb18dd7754cc *R/diagnose.R
+a9c75936f800b277e302048d41c0c133 *R/fileupload.R
 b3edd134e3395689f36cf683e096fd32 *R/globals.R
-994748f19fb745ce0639ce7072cf4508 *R/graph.R
+dba3b362ca79f6daef8cb712e844f5c9 *R/graph.R
 60c83a73e77c0aabcc30c29cfd343c10 *R/hooks.R
-aad39a0b1907f8995e9b718b48d11fe8 *R/html-deps.R
+d51c0bdc4f4fdce2044436601117c50c *R/html-deps.R
 cb5f6557eed03c0e328c4930ccb2dd5b *R/htmltools.R
 a948ed0c0e57270308f3240987fc91c8 *R/image-interact-opts.R
 937c2a6ff9c2a0becd87929301ee58a9 *R/image-interact.R
-d94b33a172fae0d748b4e76cfbf8edbc *R/imageutils.R
-6a4d4b5f6cfb96cbb863e4f526a3af4e *R/input-action.R
-afbcaea3caf81d57c48779a6148db286 *R/input-checkbox.R
-4b5191e43ffd9f67963c58351a991e97 *R/input-checkboxgroup.R
-4c817e1c1a429f9f581b59c6b1135722 *R/input-date.R
-42865b1689e7e91a57a6f4589a6b0908 *R/input-daterange.R
-d5a5e2d292b0cac11a15d7fa8de2752f *R/input-file.R
-3ce196aad434b47a4cb24cd18fcb6262 *R/input-numeric.R
-8cbb22fadcad76cb69f4d737fdd715bf *R/input-password.R
-9165141540fcf0401951ea198e89ada2 *R/input-radiobuttons.R
-ef938064772ff9c00bce77b118d6c2e5 *R/input-select.R
-1cd877552c721bd1953c513c8c0b88d5 *R/input-slider.R
+ed97714c80138da2c92742037de3d1e2 *R/imageutils.R
+7f8f6802204589ab37d06172f5940e53 *R/input-action.R
+65d9ee630649a84e145e6a520ebb9c0d *R/input-checkbox.R
+411b22116c2e1b00417eb67ef6acb720 *R/input-checkboxgroup.R
+6ec947e8d528ccb7bb88585dc02a696a *R/input-date.R
+fd55f04d9aee23afbc23ef0afc2cc4a6 *R/input-daterange.R
+3ca3eb926ae21ac315f2c964933795c2 *R/input-file.R
+fa92350905d2ebd34c9a5248d8330430 *R/input-numeric.R
+38fe4bab9adcf4e937539409cc9f9bd9 *R/input-password.R
+ac1b06b5184e16f88f346bf76f576de0 *R/input-radiobuttons.R
+4386bfe38973cc881599e8060a6269df *R/input-select.R
+ef4e0199be359a0c65a4dade30f714b5 *R/input-slider.R
 30d1369d77b3e9a87b903a0d9c6bf9eb *R/input-submit.R
-52c6c1903adeec3a5dd82fa9cc124762 *R/input-text.R
-23bd9fac114d4fe50aa9e054aa49d136 *R/input-utils.R
-6dfdd0a6d9e980e3b6f2d0d5efd29a4d *R/jqueryui.R
+b7ae016e7e83bc0b2392b8f0aaa3a1d2 *R/input-text.R
+18117fb36bd141f40a2d428b4ebde85f *R/input-textarea.R
+7eac31e4b672ae2dbfd56eb120bbe253 *R/input-utils.R
+d2d2d55307c55b74433703cff2ed2590 *R/insert-ui.R
+eda55c83ceea5d4c0c370a9d0e917c7b *R/jqueryui.R
 059f74580e15ea18075ebc75d196a4b2 *R/map.R
 271adce2a34dd07803a59bab8dc5f21e *R/middleware-shiny.R
-bbe6a683b16911ea53765767627b5714 *R/middleware.R
-e74cf2b96e000cffba17865d5278ab4b *R/modules.R
+7aa4d3e31e3d05eab6370ca2d3b98e74 *R/middleware.R
+524669ba875dfb1292dd899bf6344d7d *R/modal.R
+6d0f7f380ee6dc6c3b7db6bb1fa2a6ab *R/modules.R
+9eee0da791854d797abe756b3e557eda *R/notifications.R
 60974a727692bdfa4608c60e8a8e649e *R/priorityqueue.R
-7019c61c3228a9f40c9cc36fb1146b4b *R/progress.R
-7a02f7ca10d76b3507fe17f2c776aea2 *R/react.R
-53d29e91a9f1bd9d283d9626948fe16d *R/reactive-domains.R
-2d7e898d7223dcc7d8a1d3568c7271ac *R/reactives.R
-ec7d1ad351ec0d2acd8d8cda26dda407 *R/render-plot.R
-44e066f4d449634abcec831067b3932a *R/render-table.R
-5cd3fb30e5387154df74733c30da0bdc *R/run-url.R
-b590b0cccabd9ae0986ca1cff462b606 *R/server-input-handlers.R
-91ba5ce8608901e00a70c9d9b0810186 *R/server.R
-c9935e89cc94e6b0aba9f4edc9c82995 *R/shiny.R
-dcee8c5f31038be559581c04baa4e6b9 *R/shinyui.R
-4c42e085b055934e95705a56120c02dc *R/shinywrappers.R
-73ffd09d1f32547f81792fb033b56a3b *R/showcase.R
+90ecf0bfcbfc88f489f4f993ecbeebaf *R/progress.R
+0933ed20ab79b75e1afbd8123f907120 *R/react.R
+46c87fe2a02ddbc777432437239c121b *R/reactive-domains.R
+5de0d075d8067dd1b26128eb4751ad38 *R/reactives.R
+571d229785679a03f54b088d6a8125d7 *R/render-plot.R
+613aabcd6945abb8c642ff84bb1897e0 *R/render-table.R
+f147ee14f642fdcc6912f86d447fe4ef *R/run-url.R
+1b787135fbb2bc3548b7b2e4186cc42a *R/serializers.R
+1efda313aeaaf2e5887511d97bdc1d17 *R/server-input-handlers.R
+b9682d127a556288178f70cc00f22401 *R/server.R
+7413722439f9f9a4b384e3882e29ebb1 *R/shiny-options.R
+1a785b62fdeffd616d51ff8cec29f804 *R/shiny.R
+d604e8a5f2534729bc965473588dfe34 *R/shinyui.R
+503d9ce00c372c98bca516d38ec5e63b *R/shinywrappers.R
+31498a9cc37604d758ec61194c5041a3 *R/showcase.R
 175d3263ed5066754fb83d0894232c9f *R/stack.R
 3f61918de4e7b5aad9fedc41c12dcbfa *R/tar.R
-33647efd01263929f695dceaf24c94a0 *R/timer.R
-3720708bcbaa3ba9adccd51661916b07 *R/update-input.R
-f2951f8433533b3e697d46bdf310b9a7 *R/utils.R
-769ece9febc0d1182569fbc232fe58bd *README.md
-4fa1da11c70d19fe988bd1b0247eaa98 *build/vignette.rds
-d31f345df444a62ece470655020ee2b8 *inst/doc/events.R
-38a278f834081a0a0e64ec3e4737cdd9 *inst/doc/events.Rmd
-a82447c03ada48106a5aa26862d4a825 *inst/doc/events.html
+411796fa2119e7968003ee0f698d03f1 *R/test-export.R
+650376bbc07bcff1258d7f2a7a64243f *R/timer.R
+08367ad1506e3c67cb4694651146d43c *R/update-input.R
+71c388235d86af136b05d8b760e20d7d *R/utils.R
+afd2ecf45c8c49ed0c05d214791337ac *README.md
 13f970fb92cec9836ea297717872f46e *inst/examples/01_hello/DESCRIPTION
 85d0eaa1f8d68da015f0447469489143 *inst/examples/01_hello/Readme.md
-59c210b096161daeedde3c9d96c1efbd *inst/examples/01_hello/rsconnect/localhost/admin/01_hello.dcf
-f75977876d42e6ba7ab8997d2334b8c7 *inst/examples/01_hello/server.R
-bef815f14c41e22765178135ad3bb6fd *inst/examples/01_hello/ui.R
+2bff53f9df19d6de6bd04fe0a06c2471 *inst/examples/01_hello/server.R
+58c2edbbe1e9797317f86f1c71b822a4 *inst/examples/01_hello/ui.R
 d3b1c0d771db155d345b79c6ccf1f9c8 *inst/examples/02_text/DESCRIPTION
 5b172219d13f1f4b39ed7892197765dc *inst/examples/02_text/Readme.md
-889c5906c1b4002b61268d917fcf523e *inst/examples/02_text/server.R
-57d33bef2b340f131895092dde118170 *inst/examples/02_text/ui.R
+18ae9d566d9d287e0efff4f707bf01a5 *inst/examples/02_text/server.R
+94b942e0e52a9203b1407b257e05eab1 *inst/examples/02_text/ui.R
 33dfe9004b36e210472b5ce87154d801 *inst/examples/03_reactivity/DESCRIPTION
 65d5098f007b0aabe3f5bfb35ab134cc *inst/examples/03_reactivity/Readme.md
-850114e4e057591ced58feb247f580f7 *inst/examples/03_reactivity/server.R
-40480beab433eb2ce9e53d9ba75e411e *inst/examples/03_reactivity/ui.R
+b1235a7038404dda5f26974a0bbe545f *inst/examples/03_reactivity/server.R
+19c6b0066bb4effb47414dcfc8aa7a46 *inst/examples/03_reactivity/ui.R
 00a88b6c31a74461d091e7c924e02710 *inst/examples/04_mpg/DESCRIPTION
 9ecc57ace7f7c6e7616740bf06cce2d8 *inst/examples/04_mpg/Readme.md
-3fdd0f2956c252bb440a8baec021366e *inst/examples/04_mpg/server.R
-191b7272f64c1ae1487f7651f2562d32 *inst/examples/04_mpg/ui.R
+2f22c896468e17bab2707fcd64410d7e *inst/examples/04_mpg/server.R
+30a731a6f209338f4c39596188c581a9 *inst/examples/04_mpg/ui.R
 2c88b0033217c3596d5a9cd42888ee4f *inst/examples/05_sliders/DESCRIPTION
 9dea4065b5cef11b0a761952539d8893 *inst/examples/05_sliders/Readme.md
-219b92337ec0e5d392d6df594ffc0840 *inst/examples/05_sliders/server.R
-76d1e59959f739b0986a702035829f09 *inst/examples/05_sliders/ui.R
+4ed36a98b90af58221eb8d8cec3cf27b *inst/examples/05_sliders/server.R
+16b8fab7da19759737d3a6414375a77a *inst/examples/05_sliders/ui.R
 f601f0341132edd28266937c9f9fcd8f *inst/examples/06_tabsets/DESCRIPTION
 f3c2f11e2df951548bdb75cbaf19946a *inst/examples/06_tabsets/Readme.md
-f8247b4719548dc457ed8222d6711386 *inst/examples/06_tabsets/server.R
-1a4c26c96f2606be4b2443970bc33b3f *inst/examples/06_tabsets/ui.R
+fdab09eb2c2be970e9a92f5762707252 *inst/examples/06_tabsets/server.R
+06feed1ca040463292775e0369c86b42 *inst/examples/06_tabsets/ui.R
 448d49f3c1fd4f84bf6affd027605023 *inst/examples/07_widgets/DESCRIPTION
 09131b23106933be28474b5ce6161224 *inst/examples/07_widgets/Readme.md
-2ad99ad701855015c10dd6e7849a6056 *inst/examples/07_widgets/server.R
-2b3ba17a56b2f3d40fe0c21c12ca6dee *inst/examples/07_widgets/ui.R
+671c7bf33d459552cba1eb0b93faa076 *inst/examples/07_widgets/server.R
+e8d08f013b24e6c471d619fd34184750 *inst/examples/07_widgets/ui.R
 fc57e0cc1f1689531e8b6d2b308530a6 *inst/examples/08_html/DESCRIPTION
 5554a23aa8a10b633f5fddfe3d159d57 *inst/examples/08_html/Readme.md
-57eb0e039a92f909c64e5ed3e7fb7545 *inst/examples/08_html/server.R
+41124d8f6c1a9895786583462d83bee4 *inst/examples/08_html/server.R
 0785b66c45a23e001192e7a238473cee *inst/examples/08_html/www/index.html
 12d0f805ce58a70a7f69505c53617108 *inst/examples/09_upload/DESCRIPTION
 0ab651ad98f08f34e0589f69456c11f2 *inst/examples/09_upload/Readme.md
-c48a2d96478ea5080f9f96cdeb2ec9a2 *inst/examples/09_upload/server.R
-07bd7351f38167707b5ec0b380debf7d *inst/examples/09_upload/ui.R
+283b2bae49c7f61e6bf8bc0eb1ebbae6 *inst/examples/09_upload/server.R
+25d426339fc3db342bc075c64aa3a22a *inst/examples/09_upload/ui.R
 6e681f3bb2f604aa20ee1b0a32ccaf55 *inst/examples/10_download/DESCRIPTION
 e17525eec6c34fcdcb81db74a73185a6 *inst/examples/10_download/Readme.md
-74de97fd34458c4c4f03b923e19928ed *inst/examples/10_download/server.R
-968a135d110673c32909ab7f2b81c008 *inst/examples/10_download/ui.R
+d762e479421f843d01ff968dae55e327 *inst/examples/10_download/server.R
+608192108ae87e9a847a5aa11cd2c129 *inst/examples/10_download/ui.R
 c481010dc652e7a3ab80140f461c0ef2 *inst/examples/11_timer/DESCRIPTION
 f1b063aae380d9463f3739dd60c3e666 *inst/examples/11_timer/Readme.md
-4bcd28c743dc6d728fc448279b0732dc *inst/examples/11_timer/server.R
-fbbf7a19bfddc49480583df89261819c *inst/examples/11_timer/ui.R
-387c11d96f7f1347d1e17156fcdea7ea *inst/staticdocs/index.r
+35d7c446405450684e2acef391dae83f *inst/examples/11_timer/server.R
+41f3b74ab8de992e28301ee763af45e5 *inst/examples/11_timer/ui.R
+1343514b5c91235dbc24b47144f4d73e *inst/staticdocs/index.r
 cd58550659401cbeb23bc3dad25399b4 *inst/template/default.html
-b2374b918ca49c287bd791343f46016f *inst/tests/test-bootstrap.r
-dbe84e866824cfdf042f6d92729188cf *inst/tests/test-gc.r
-8ae16d4481858309a4625678e2b8e040 *inst/tests/test-input-handler.R
-f9ee5309105f8f0c9743572e7409336f *inst/tests/test-modules.R
-757144c90564ef939292d6ad7222135f *inst/tests/test-plot-coordmap.R
-f44284201cd8fced1d525c7ae87d4c34 *inst/tests/test-reactivity.r
-bc84dab9f44f9079568a0c2e4fc83884 *inst/tests/test-stack.R
-399ab41ea5c56acca74c37949699d101 *inst/tests/test-stacks.R
-8070321db7e859f42cfed7753b9234b0 *inst/tests/test-staticdocs.R
-6269cd71b94d9dca02179dfa20aeea3b *inst/tests/test-stop-app.R
-edce45a03c0b1ffe775546374c10aed0 *inst/tests/test-text.R
-c59f5d981a17ad5c7e93f367abb45a1b *inst/tests/test-ui.R
-49db9cb619120b4874f8f8d85641d0ad *inst/tests/test-url.R
-b5978ec26571bd4116d061b90994a70d *inst/tests/test-utils.R
 c6e3068736b084cd7d78d38452ced423 *inst/www-dir/index.html
-58dfa81d2d07f55f54e924d75edca38f *inst/www/reactive-graph.html
-659231dde1c53bd14bbcffa1456df892 *inst/www/shared/bootstrap/css/bootstrap-theme.css
-ca7ee393ea214c018d316eee3acc7f39 *inst/www/shared/bootstrap/css/bootstrap-theme.css.map
-bf3499da1c31113720e9e395691730ba *inst/www/shared/bootstrap/css/bootstrap-theme.min.css
-957474c344c7131fb8e093449cc4893a *inst/www/shared/bootstrap/css/bootstrap.css
-ea05728a43eaff288b1d535ebe89ec25 *inst/www/shared/bootstrap/css/bootstrap.css.map
-5d5357cb3704e1f43a1f5bfed2aebf42 *inst/www/shared/bootstrap/css/bootstrap.min.css
+fc07eb5cb6354d9070657e2cf0df3658 *inst/www/reactive-graph.html
+0651d338f968e5b2e9f256d066542e88 *inst/www/shared/babel-polyfill.min.js
+b9b46bcc4dad6cc90fc4f95073c50735 *inst/www/shared/bootstrap/css/bootstrap-theme.css
+d6cc0a3c7532b74efe92da8992bd7542 *inst/www/shared/bootstrap/css/bootstrap-theme.css.map
+ab6b02efeaf178e0247b9504051472fb *inst/www/shared/bootstrap/css/bootstrap-theme.min.css
+5f587b800215ee268efd2676ac010b25 *inst/www/shared/bootstrap/css/bootstrap-theme.min.css.map
+2a31dca112f26923b51676cb764c58d5 *inst/www/shared/bootstrap/css/bootstrap.css
+c7ffd8c7a285780039b2366034f3bca2 *inst/www/shared/bootstrap/css/bootstrap.css.map
+ec3bb52a00e176a7181d454dffaea219 *inst/www/shared/bootstrap/css/bootstrap.min.css
+35b79ebe0b7805c1c84524ad920faa33 *inst/www/shared/bootstrap/css/bootstrap.min.css.map
 f4769f9bdb7466be65088239c12046d1 *inst/www/shared/bootstrap/fonts/glyphicons-halflings-regular.eot
 89889688147bd7575d6327160d64e760 *inst/www/shared/bootstrap/fonts/glyphicons-halflings-regular.svg
 e18bbf611f2a2e43afc071aa2f4e1512 *inst/www/shared/bootstrap/fonts/glyphicons-halflings-regular.ttf
 fa2772327f55d8198301fdb8bcfc8158 *inst/www/shared/bootstrap/fonts/glyphicons-halflings-regular.woff
 448c34a56d699c29117adc64c43affeb *inst/www/shared/bootstrap/fonts/glyphicons-halflings-regular.woff2
-8015042d0b4ac125867af5b096b175ce *inst/www/shared/bootstrap/js/bootstrap.js
-4becdc9104623e891fbb9d38bba01be4 *inst/www/shared/bootstrap/js/bootstrap.min.js
+fb81549ee2896513a1ed5714b1b1a0f0 *inst/www/shared/bootstrap/js/bootstrap.js
+5869c96cc8f19086aee625d670d741f9 *inst/www/shared/bootstrap/js/bootstrap.min.js
 ccb7f3909e30b1eb8f65a24393c6e12b *inst/www/shared/bootstrap/js/npm.js
 ae8fceae0e07d55b5cfaa9af6bc43a6d *inst/www/shared/bootstrap/shim/html5shiv.min.js
 506fe393e9f296d14b63733c0aff6205 *inst/www/shared/bootstrap/shim/respond.min.js
@@ -148,60 +142,88 @@ d48475e6c742940f44e62622e16865b9 *inst/www/shared/datatables/images/sort_desc_di
 24f79934c309c1b44c01bfd87a11c07c *inst/www/shared/datatables/js/dataTables.bootstrap.js
 1008c5821adb81361143e958a81c3def *inst/www/shared/datatables/js/jquery.dataTables.min.js
 bd39f6bbd365592c78cbd0be915143b5 *inst/www/shared/datatables/upgrade1.10.txt
-4ccf25015fb27dfe73cc8c271b6306ff *inst/www/shared/datepicker/css/datepicker.css
-6c009bb69a14d93a4a5a6aafddf6ce7c *inst/www/shared/datepicker/js/bootstrap-datepicker.js
-0294fa97e35d954d211cdafb3437e114 *inst/www/shared/datepicker/js/bootstrap-datepicker.min.js
-e60dc33e00b59deffc85f7cb5aaf2547 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.bg.js
-b652ea76f0458fc7d82f923a45e49d08 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ca.js
-d51e1ef0a1881405df2fa57285f83c8c *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.cs.js
-8a4de4e566b09982f37a8994f1cae2e1 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.da.js
-48e678c0b45c66ace3784a6d7f9d06cf *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.de.js
-6c9517419e35fa5869eed851a85a2a62 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.el.js
-ee171e671d27bd6a4960971eb85b63ea *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.es.js
-db42143ef2b7ec1316707b379464b185 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.fi.js
-f7cb5c79690dc3a459c4557999411f0c *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.fr.js
-7476419aac21a158b471da79b0444a73 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.he.js
-d2fd6f2029d157b872660dc1d4bcf817 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.hr.js
-7f80f4a105256c431d21fed6fb9f2085 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.hu.js
-fca5c01a9b080b0643c92211cfd0bc4c *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.id.js
-0bcffcd9483f5fa8c893fe5f75b0d870 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.is.js
-1b91206d89bf235afb67df1c869c5aab *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.it.js
-edc2c85ed0bc3c2b74b61bfb4622ada8 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ja.js
-1fe72afc53aa0abdd276f7708777cd0f *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.kr.js
-4c7f87bb362455118fc8b0cfcd9ac7b5 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.lt.js
-0cc44acf6d037d9917510ad302e8ee17 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.lv.js
-d31bb70d0a394f211700a5d4a3f08c78 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ms.js
-49a64a01043ebe272a00d274817de57f *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.nb.js
-f5fe1c76037f7415d78a3e17028b19c6 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.nl.js
-dd4533c688c9c18d2cee1a0c119f9728 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.pl.js
-f0c73b00644fb731da5367a8ddf0aa8e *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.pt-BR.js
-c22be1f66034d310cd00a8f713328583 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.pt.js
-c69b7e00fbd761418c5362ba1bff7a81 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ro.js
-b391bf9e41d343cdfde857e3de4ed048 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.rs-latin.js
-83ccd03ad063aef954d61a244891b635 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.rs.js
-74d309adf7af3a880f6edbe1a2175c21 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ru.js
-f425a563a1aa2c067b45309785122fad *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.sk.js
-5d6f3e7bed8dc41eae685ed652d5ce85 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.sl.js
-278d86692d764823b508b1fa32f34bbc *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.sv.js
-2f01204f639b304ca55f44eb4ba2c0fe *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.sw.js
-2ce9201bd103d1ce6481943462c71b42 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.th.js
-839c1b4eb3d40f1a4cfddbf45db6ffde *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.tr.js
-19915303ddbfa4df040c236437ab01d9 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.uk.js
-88b7aa4a335c3dc7415614574467b54e *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.zh-CN.js
-5326218ac27d3c714cb642a703130e01 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.zh-TW.js
-bf8b245db2b4f4d282faa96b70a8c3b7 *inst/www/shared/datepicker/less/datepicker.less
-5343ee1a287a65ff20961476fd8a6188 *inst/www/shared/font-awesome/css/font-awesome.css
-4fbd15cb6047af93373f4f895639c8bf *inst/www/shared/font-awesome/css/font-awesome.min.css
-87d8ca3ddc57e7d2da6226e480f90457 *inst/www/shared/font-awesome/fonts/FontAwesome.otf
-32400f4e08932a94d8bfd2422702c446 *inst/www/shared/font-awesome/fonts/fontawesome-webfont.eot
-f775f9cca88e21d45bebe185b27c0e5b *inst/www/shared/font-awesome/fonts/fontawesome-webfont.svg
-a3de2170e4e9df77161ea5d3f31b2668 *inst/www/shared/font-awesome/fonts/fontawesome-webfont.ttf
-a35720c2fed2c7f043bc7e4ffb45e073 *inst/www/shared/font-awesome/fonts/fontawesome-webfont.woff
-db812d8a70a4e88e888744c1c9a27e89 *inst/www/shared/font-awesome/fonts/fontawesome-webfont.woff2
+be8fd7a445f9fbbf62f47f2c6e7d257a *inst/www/shared/datepicker/css/bootstrap-datepicker3.css
+5998447833daa2c4620be9686167d715 *inst/www/shared/datepicker/css/bootstrap-datepicker3.min.css
+be74d5befb57a55bcf71d5af775f8dfb *inst/www/shared/datepicker/js/bootstrap-datepicker.js
+04044606b54ed4a1cb471d54ff35713c *inst/www/shared/datepicker/js/bootstrap-datepicker.min.js
+35d26b024f96e3fb2af435c7c0f31190 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ar.min.js
+000e804c2c7e399be57a934139d0d4b0 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.az.min.js
+54d489f29cbff19d419a47d9373d4528 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.bg.min.js
+09824a399a5a7a07c41c3bfccb767b66 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.bs.min.js
+140d8ef13cbaa5915be82edca7b2e9ea *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ca.min.js
+0b84b83453d3eb32085a196ec1e45162 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.cs.min.js
+4c19733f05237657eadc42cead63607c *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.cy.min.js
+4618274b5408a27294af1b0b4133534d *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.da.min.js
+e379a61bac6fb9cb1432ae048c00a2d4 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.de.min.js
+9a8c3f87f7e656bae9d7f4f495d34ad0 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.el.min.js
+c547ee603fe716b417fa3318703e1775 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.en-AU.min.js
+0aff1a27d7e87aa4d7162b0928415adc *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.en-GB.min.js
+f278d2b5d00aeeaad81ae6a30561dfb3 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.eo.min.js
+0c240809f25d1bf69a78e589d81b15fd *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.es.min.js
+bdc157716069be8880b19d5e1663d131 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.et.min.js
+2cea5a417e54dd16189a79d48532eaa5 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.eu.min.js
+f9dca456507865d74367df2ec21442d2 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.fa.min.js
+3b2ea6b77ec81a3b049ad67fbd61da65 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.fi.min.js
+542de91da4d0304fdd2f85dd25c259fa *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.fo.min.js
+8e16f409b6c4cd9f89dc39e328f0a804 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.fr-CH.min.js
+d69412ba9c9add964acbd11f0c7ab3d4 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.fr.min.js
+570781c190a59487dd5fdf037df3f46c *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.gl.min.js
+7e36ca89762611a0fac1d8d3d03b4235 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.he.min.js
+5eeab995783abb983baa504e9ad38d4b *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.hr.min.js
+2fca7eb28145674f456a59186c8acd4e *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.hu.min.js
+bd397db07229b0126e2168f6aa38191d *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.hy.min.js
+77beed0d1402226bdaec79a8423c6bef *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.id.min.js
+8d2b24a898aa0a026f38e41ea25295e9 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.is.min.js
+29a2e65ee680766d46522fd5b885f1c1 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.it-CH.min.js
+7440f71697670ae6215d6f1f68bf6eac *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.it.min.js
+458a858a3d60a0a11bdaa1da796c0d95 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ja.min.js
+9c7b2a6b2b224210f425556fc46fb4ff *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ka.min.js
+4176a7944ccde5f13d463cb3d62dacfb *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.kh.min.js
+66e7ca803db3b023413cb3a2257c2ed9 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.kk.min.js
+4cd53b7aaff6c01d5476db7ef36b15b7 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ko.min.js
+e51eb36be5d0009b5bc8234eab371f65 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.kr.min.js
+1d075794044f3c1f8097b7411cb160ea *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.lt.min.js
+5ddb0f077a652836582daf9b297b819c *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.lv.min.js
+ee094dcc6b080440ae648f0b5f551fd7 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.me.min.js
+8ad641a1b0df58643675e914757222d6 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.mk.min.js
+9053a24b169a02078f62c87db61929e0 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.mn.min.js
+558e364314f7a6f1e8ff552af45289a0 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ms.min.js
+977b71a3cd932b731c138f8e705d101f *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.nb.min.js
+d102d2544ac0c83ae7becd89db845a3e *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.nl-BE.min.js
+7ea61899ebdc63516fa9390c2960422c *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.nl.min.js
+cbe9d87ffe7c3a8b93cfe1baee7dfdb2 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.no.min.js
+6e5d48470a01fe340e8aab7d07bdf36c *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.pl.min.js
+d89e56a0947bc625e6d0afa7336388bc *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.pt-BR.min.js
+5fac70c505674c265acd14fe205fc28c *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.pt.min.js
+7ef7c427e5a6dadf980ca012a929fce8 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ro.min.js
+1792f11adb32653e6a38a30fa026e55f *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.rs-latin.min.js
+9d7f340bb0b1f94c6ebe29ca4f17cd08 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.rs.min.js
+54c6aa69effea960877094695f403d30 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ru.min.js
+ed7d6fc7c720724a01cf06b4c9987b23 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.sk.min.js
+8b97accd7dccbaac6119fec87fe2a09a *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.sl.min.js
+998eeef54bf839c6470da58e285a1237 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.sq.min.js
+a00919b4647f5082db5030577152c94a *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.sr-latin.min.js
+b8db81ddacac2fe29227b2f3b104feda *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.sr.min.js
+4e14590b2ac964e211853079aeb5bc32 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.sv.min.js
+660f1add7129dd1e357d349e51e9ba3b *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.sw.min.js
+6ecf92ecfc65cd7cb1ba60a32aa950c9 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.th.min.js
+96d8b009eebbd4a96fd3c99977dc9ecb *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.tr.min.js
+ff0781daf30ebf3a9bff3e2c29e3bc36 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.uk.min.js
+8be76c0d0e10acd89d19249e5830d072 *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.vi.min.js
+4a616de93d45ec1d42f9bfab4ea2689e *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.zh-CN.min.js
+7c30c64ca6cebaa76e783c22988f39bc *inst/www/shared/datepicker/js/locales/bootstrap-datepicker.zh-TW.min.js
+b652e3b759188ceaf79182f2fe72ea64 *inst/www/shared/font-awesome/css/font-awesome.css
+4083f5d376eb849a458cc790b53ba080 *inst/www/shared/font-awesome/css/font-awesome.min.css
+5dc41d8fe329a22fa1ee9225571c843e *inst/www/shared/font-awesome/fonts/FontAwesome.otf
+25a32416abee198dd821b0b17a198a8f *inst/www/shared/font-awesome/fonts/fontawesome-webfont.eot
+d7c639084f684d66a1bc66855d193ed8 *inst/www/shared/font-awesome/fonts/fontawesome-webfont.svg
+1dc35d25e61d819a9c357074014867ab *inst/www/shared/font-awesome/fonts/fontawesome-webfont.ttf
+c8ddf1e5e5bf3682bc7bebf30f394148 *inst/www/shared/font-awesome/fonts/fontawesome-webfont.woff
+e6cf7c6ec7c2d6f670ae9d762604cb0b *inst/www/shared/font-awesome/fonts/fontawesome-webfont.woff2
 ec39d75cbc4de8171c2f6656a26816a3 *inst/www/shared/highlight/LICENSE
 e095ae120d1b8a9c12d0ebf7e7fa967b *inst/www/shared/highlight/classref.txt
-ef820cc4df5e3628daa70374ee375232 *inst/www/shared/highlight/highlight.pack.js
-153aee4df2efe90761a9d387ef768805 *inst/www/shared/highlight/rstudio.css
+0b5415083b347c7d5f98b47bcf7e758a *inst/www/shared/highlight/highlight.pack.js
+097a443771d9f9e78d203331be166d3e *inst/www/shared/highlight/rstudio.css
 2ae68042ac97e2b2213b713deece387f *inst/www/shared/ionrangeslider/css/ion.rangeSlider.css
 8d631a7cac12ac3c47d8bfe67f896543 *inst/www/shared/ionrangeslider/css/ion.rangeSlider.skinFlat.css
 7527c2a31899e27ddbc0fcba8dfc3b8d *inst/www/shared/ionrangeslider/css/ion.rangeSlider.skinHTML5.css
@@ -214,179 +236,223 @@ bcdb14f38e27b16edeabb62e6e9a829b *inst/www/shared/ionrangeslider/img/sprite-skin
 6035779c2555ab87be45bbc21f9cae47 *inst/www/shared/ionrangeslider/img/sprite-skin-modern.png
 41732f58be91fcdc79381f239685c0e1 *inst/www/shared/ionrangeslider/img/sprite-skin-nice.png
 43c6858e46da90d6e201dfed860e8b86 *inst/www/shared/ionrangeslider/img/sprite-skin-simple.png
-afd8906dc335dcb7d9b81386ed24d67a *inst/www/shared/ionrangeslider/js/ion.rangeSlider.js
-70878da4eb2660092e533750420bbb45 *inst/www/shared/ionrangeslider/js/ion.rangeSlider.min.js
-12b40fddbb08ec43e278e7d8a0ab5543 *inst/www/shared/jquery-AUTHORS.txt
-7f38dcbfb11aff050652ff3b754adb63 *inst/www/shared/jquery.js
-895323ed2f7258af4fae2c738c8aea49 *inst/www/shared/jquery.min.js
-c34c7baffbb76616a99568e01f27930e *inst/www/shared/jquery.min.map
-209ed8e314e0e913f7d2eb5c9e5ec785 *inst/www/shared/jqueryui/README
-d80747d34afb87753b51d45a36f8bd17 *inst/www/shared/jqueryui/images/ui-icons_444444_256x240.png
-1e70a2d46244ee4070fd47152bd71db1 *inst/www/shared/jqueryui/images/ui-icons_555555_256x240.png
-eb4a975c630f379279fe78c604d0b36c *inst/www/shared/jqueryui/images/ui-icons_777620_256x240.png
-68855e6e3d288ab126a1f1dd82b64e26 *inst/www/shared/jqueryui/images/ui-icons_777777_256x240.png
-7c81a6253b77a9fdaf51bb1038f8c840 *inst/www/shared/jqueryui/images/ui-icons_cc0000_256x240.png
-41612b0f4a034424f8321c9f824a94da *inst/www/shared/jqueryui/images/ui-icons_ffffff_256x240.png
-35c5b4c500d766e04a9b3a7b05701b2e *inst/www/shared/jqueryui/index.html
-a14ebdbb782f62a97b0bfc1d0749e138 *inst/www/shared/jqueryui/jquery-ui.css
-4156bf51d2d5144087fd8766e433a3d8 *inst/www/shared/jqueryui/jquery-ui.js
-a0fd52784e4da798e7d6a4b52e991db1 *inst/www/shared/jqueryui/jquery-ui.min.css
-991c88f48d11ebce6828a6b2be17dad2 *inst/www/shared/jqueryui/jquery-ui.min.js
-c762a2f34b42e912713542ebdc16ba4b *inst/www/shared/jqueryui/jquery-ui.structure.css
-c68383f6186ce6de68fdb44abdfdebce *inst/www/shared/jqueryui/jquery-ui.structure.min.css
-f05390e630b6c940d8d86c65596f62f5 *inst/www/shared/jqueryui/jquery-ui.theme.css
-d3ea6df653da3d8ac16cc34452214d52 *inst/www/shared/jqueryui/jquery-ui.theme.min.css
+d4b2be381ee15900642f9ab06c9bfc65 *inst/www/shared/ionrangeslider/js/ion.rangeSlider.js
+ec750d838b13f4e4c8b79dcd89c098a8 *inst/www/shared/ionrangeslider/js/ion.rangeSlider.min.js
+f3b07ee7912d1f78c0535a919e4f192c *inst/www/shared/jquery-AUTHORS.txt
+fb2d334dabf4902825df4fe6c2298b4b *inst/www/shared/jquery.js
+4f252523d4af0b478c810c2547a63e19 *inst/www/shared/jquery.min.js
+b40722fa1b5fa066562beab1c8c0e0b3 *inst/www/shared/jquery.min.map
+403511fc6c430ea6179ebcb4a9983eec *inst/www/shared/jqueryui/AUTHORS.txt
+e0c0d3d883e83f19efa64feb54d5f63d *inst/www/shared/jqueryui/LICENSE.txt
+b5bc162f6c9d6f95688604465a526fd6 *inst/www/shared/jqueryui/README
+d10bc07005bb2d604f4905183690ac04 *inst/www/shared/jqueryui/images/ui-icons_444444_256x240.png
+00dd0ec0a16a1085e714c7906ff8fb06 *inst/www/shared/jqueryui/images/ui-icons_555555_256x240.png
+4e7e3e142f3939883cd0a7e00cabdaef *inst/www/shared/jqueryui/images/ui-icons_777620_256x240.png
+40bf25799e4fec8079c7775083de09df *inst/www/shared/jqueryui/images/ui-icons_777777_256x240.png
+093a819138276b446611d1d2a45b98a2 *inst/www/shared/jqueryui/images/ui-icons_cc0000_256x240.png
+ea4ebe072be75fbbea002631916836de *inst/www/shared/jqueryui/images/ui-icons_ffffff_256x240.png
+11f31c348968868d4ada89d6162c640e *inst/www/shared/jqueryui/index.html
+6fd5a6e8197041971d02cf62d06f4b14 *inst/www/shared/jqueryui/jquery-ui.css
+ab5284de5e3d221e53647fd348e5644b *inst/www/shared/jqueryui/jquery-ui.js
+0b5729a931d113be34b6fac13bcf5b29 *inst/www/shared/jqueryui/jquery-ui.min.css
+c15b1008dec3c8967ea657a7bb4baaec *inst/www/shared/jqueryui/jquery-ui.min.js
+b75d9919ded1b610c51a4727dd4b9720 *inst/www/shared/jqueryui/jquery-ui.structure.css
+5581d20aa5062ed5c0b6048f68e76055 *inst/www/shared/jqueryui/jquery-ui.structure.min.css
+c4679db4adb3f9b8089213c6c396e2ee *inst/www/shared/jqueryui/jquery-ui.theme.css
+c12cac44216cf877fd0c6903f3794407 *inst/www/shared/jqueryui/jquery-ui.theme.min.css
 1439535f0398dc7b2d17f487302af165 *inst/www/shared/json2-min.js
 d75b17ebe7200b2ff0b8d20d32853c35 *inst/www/shared/selectize/css/selectize.bootstrap3.css
 bd681a8efd2f45628dddf749426dc633 *inst/www/shared/selectize/js/es5-shim.min.js
 146435eeda32f0e12bca8519f0da5ad9 *inst/www/shared/selectize/js/selectize.min.js
 c539894c4785ce273996b9d5376c048e *inst/www/shared/shiny-showcase.css
-1fa4b0932c3138a1fec9b30815926474 *inst/www/shared/shiny-showcase.js
-e9806de66ff7cd1d467398bf4ebe85e2 *inst/www/shared/shiny.css
-31c36e3988c8953815890b3c7f0ecfdb *inst/www/shared/shiny.js
-32f6d3f636bef81ad5c226a784ab540b *inst/www/shared/shiny.js.map
-6d56c9dca6bf55204b4d07a6b32e08c5 *inst/www/shared/shiny.min.js
-71859b3be5df9fd01f44c25f2c3b0813 *inst/www/shared/shiny.min.js.map
+226233db9edba2c3503b0df84ef8a1cc *inst/www/shared/shiny-showcase.js
+0a9b375649f25d6201c0867df340f1a1 *inst/www/shared/shiny.css
+dabba789395e9f4d568e47a922c5fde3 *inst/www/shared/shiny.js
+934c7e1052307cea2239a0d1f5f0682b *inst/www/shared/shiny.js.map
+d07ead81941c0093ec4435ffcfc45ca5 *inst/www/shared/shiny.min.js
+1400f8989bd7e5e183c8f42d1d2198f7 *inst/www/shared/shiny.min.js.map
 d42bccda0accb4e937f79e5bd9540643 *inst/www/shared/showdown/compressed/showdown.js
 ae18a8f49f1a446ca1772447b95a99d0 *inst/www/shared/showdown/license.txt
 e192e62c348b4dc09fd3d9f32cd558c3 *inst/www/shared/showdown/src/showdown.js
 4feedff422885d51bf57601f8a987d70 *inst/www/shared/strftime/strftime-min.js
 d8ccfa9e1bad66e47dbd88a663fb5a86 *man/HTML.Rd
 c272996ea1fd62ed42113b3c4a9c06e6 *man/NS.Rd
-607bebf85c358163154b741d9b9e8e37 *man/Progress.Rd
+6b695cf8aa4e2e2f9000350dfa35ee63 *man/Progress.Rd
 9b645260993b8a9dba0e1de99dfeb47d *man/absolutePanel.Rd
-ca5bdc7dbcd9dbd23ec54a7bd2f5d18c *man/actionButton.Rd
-8e89eae6a98f49f7fb4f30f78a7aa8ed *man/addResourcePath.Rd
+0983de4f0d434fb88e8a2179dee83ab2 *man/actionButton.Rd
+37baaab9d4b9159fd70d1a507391e447 *man/addResourcePath.Rd
+f23a4205ab93ffde15bd44ba1404ea5d *man/applyInputHandlers.Rd
+d38b6b09e59d71964cd040c0b0d063a1 *man/bookmarkButton.Rd
 5c7e3991f2c6c68b4376d9d03c86f682 *man/bootstrapLib.Rd
 0ce227618e7e1c463a235374c8bdfa9d *man/bootstrapPage.Rd
 d1641fa3516d97c0890cca51a88e2586 *man/brushOpts.Rd
 a64dfa56528185a338efd8438e97ac3f *man/brushedPoints.Rd
 22a8bcc9aa7da08a6ca8bef449257da1 *man/builder.Rd
 9e712890a20931b166ec8a82545deae7 *man/callModule.Rd
-b718ea9c70e2bed65b3e4f64ffae8c59 *man/checkboxGroupInput.Rd
-6a96dcf8c78ca1a2cb5ad8621bcba3ae *man/checkboxInput.Rd
+df6def08194f9c22e0b70afd57ed5689 *man/checkboxGroupInput.Rd
+57c8638b00ed42a76290d99bb2ab86e3 *man/checkboxInput.Rd
 5c6fd00763d20fa97fc89a955913a631 *man/clickOpts.Rd
-443a5fd82e84b8f14b01ad6228415537 *man/column.Rd
-d4bddb39893583807389d3b68e8cacc3 *man/conditionalPanel.Rd
-4df30769b892d6efd622d71fc9e8d69d *man/createWebDependency.Rd
-d0efb69c847e97b37b22dbb1eedb9ee4 *man/dateInput.Rd
-7556476f2322c9cad9e8c39abbdb5f93 *man/dateRangeInput.Rd
+9bacf8c019a10445b779a3fc0c64269e *man/column.Rd
+6c9870829d5833a37428af1cfd010461 *man/conditionalPanel.Rd
+f3c0810329c410cc843f5c6e9d440e16 *man/createWebDependency.Rd
+071d37b1e519fd797449506703c769cb *man/dateInput.Rd
+46978f653eecb1c32a39e18adfdb5d32 *man/dateRangeInput.Rd
 a941504ef1e6235ce5161b26cd207543 *man/dblclickOpts.Rd
 2d9b353d16b677b55a58ff0cff020541 *man/domains.Rd
 481fa1943f6d4353eeed023a75521446 *man/downloadButton.Rd
-e1904616d3471a528f2eba8a67056957 *man/downloadHandler.Rd
-6cfe34fd8c90c75d59b1147587679960 *man/exprToFunction.Rd
-10d162d84b04c9f73ee4345462d835e2 *man/fileInput.Rd
-f3b54e62a2d5e862048d4c951d2f3604 *man/fillPage.Rd
-ecbbb7a17d0d51a46b2b665b8e13cfe9 *man/fillRow.Rd
-71e5cf2843cc221d3ff8dae21fca30e9 *man/fixedPage.Rd
-9a4179ba2d1d6924b748f4e6cfa425e0 *man/flowLayout.Rd
-1acec4d9ad52139cb9df1506d4d1c58d *man/fluidPage.Rd
+b1b0171792a1332a9fc950fc05feea3c *man/downloadHandler.Rd
+1ad2440a33bd4cfb1abed64535c81bb6 *man/enableBookmarking.Rd
+32cd02cd61aa1aca2911eb8a87bf9009 *man/exportTestValues.Rd
+a285c035ff015a2575f8219c4ad6aa6d *man/exprToFunction.Rd
+6baa248b78954215db322d3853be3cbd *man/fileInput.Rd
+7e7ce1a49c339b757eda2facaecf3292 *man/fillPage.Rd
+843a9cfba2fe717c74b5b23580ececb7 *man/fillRow.Rd
+f380c5aa9dc647d7f9b9ffbf7ee0a251 *man/fixedPage.Rd
+e583f217b68ed5f8e6f1b54a78846d1c *man/flowLayout.Rd
+eccc5ddd4b6d2efa6b366b19983d293d *man/fluidPage.Rd
+1e7a5118508425f723db518ce5ef10e7 *man/freezeReactiveValue.Rd
 56046edbee3dff3739e86ebdf2feec48 *man/headerPanel.Rd
 a4ba8d8c599204740128fea8b6711029 *man/helpText.Rd
 b7e98aeab986df2f38906b9810c43ecb *man/hoverOpts.Rd
 406342e1835b163037604718f4d2be33 *man/htmlOutput.Rd
 b6db1c34fc97f2fa8fb24f7d2713aec5 *man/htmlTemplate.Rd
-1523581bdab30606e35ccac2df72a699 *man/icon.Rd
+6ae1540704407e789762a0fc97f21fe6 *man/icon.Rd
 82b947ce74f2de68a3d3d9f40c73770d *man/include.Rd
 669379d570b9756032d5739598566053 *man/inputPanel.Rd
+9c3b0afb61ab3434e56b0f2b3b6eee8f *man/insertUI.Rd
 388595ea019cb50b911585a0e83385ab *man/installExprFunction.Rd
-71ac86bad0f944c3b793ecce413f9876 *man/invalidateLater.Rd
+e076609c816c0988da5c2be9198fc820 *man/invalidateLater.Rd
 c4df81c50b02bbab02a021451f779900 *man/is.reactivevalues.Rd
-72415c47cac67d5f535798898ec186c9 *man/isolate.Rd
+1f36b2c405bd2c6eff6780b345030f34 *man/isolate.Rd
 e6422e37aa4e65a851ea929c44537daa *man/knitr_methods.Rd
 0cef753d073f90ea736a0b259cb53c3f *man/knitr_methods_htmltools.Rd
 cbc1ae1138599f3156f30fea5d91a148 *man/mainPanel.Rd
 5a7e974ba5c1909aea3940f62535e5be *man/makeReactiveBinding.Rd
-305ccc6c35d8b20ca1dc4698be46f456 *man/markRenderFunction.Rd
+4833824d625986e1b6cf9c38803dabb9 *man/markRenderFunction.Rd
 658fedea2ffa8f76b52ed1c5f70b02e9 *man/maskReactiveContext.Rd
-c93dfe1027a2e5afad1351b4f1f6d894 *man/navbarPage.Rd
-fe8ca5a744d2e612be1acf8d8544707d *man/navlistPanel.Rd
+198500a6aad23a6f97d9b4440cf25db2 *man/modalButton.Rd
+7a8bda204c8d2e590f36118a4678c918 *man/modalDialog.Rd
+6ef0d9c2932cbc94fbdcfb2ec55c55bb *man/navbarPage.Rd
+6a5f9a365314215f24b6fdac95819f81 *man/navlistPanel.Rd
 c5093ba7b905be444ec88e92c96601fd *man/nearPoints.Rd
-0e8ed4ed87b4e65dce218e5fe7f42b29 *man/numericInput.Rd
-b37b27f3eb2f1ce872125e62f405297d *man/observe.Rd
-76fe564c675685a80333f47b7b892078 *man/observeEvent.Rd
+6425976695c5876714e9aafb25dd4e56 *man/numericInput.Rd
+502664a517e3252a2ecd47ffd12657b7 *man/observe.Rd
+6b39b917fef923fa748fad5ed1819a14 *man/observeEvent.Rd
+775a1e9289ca94c74b5bde1cb7570bec *man/onBookmark.Rd
+a6066c56c39f8ef669f6afa7b0d14665 *man/onFlush.Rd
 5f97903060489ca6bf90931b1efd434c *man/outputOptions.Rd
-d4e8e4348d1afd23f62ea83f6ca1aad6 *man/pageWithSidebar.Rd
-892272311c02f31faee43411c957b8b1 *man/parseQueryString.Rd
-36728b593ee1f018714c94ac3ee4bf4a *man/passwordInput.Rd
+64956bda426d937ece36462ce7d64664 *man/pageWithSidebar.Rd
+32188a9e53789dca0e17a0956f2c5e0f *man/parseQueryString.Rd
+81fe1abb4ed16abcfb3cb33051438adc *man/passwordInput.Rd
 08dc2bd952ad061b03ef87da4be22ed6 *man/plotOutput.Rd
 2aeaf22fa666cd0c5415d806365e138b *man/plotPNG.Rd
-6f2e3919adcd492cc02f596cad02eb13 *man/radioButtons.Rd
-fdaa5a696d5e946f4c4bea723d8675f2 *man/reactive.Rd
-9d62d597fc10bcc68af621f3ca1512f3 *man/reactiveFileReader.Rd
+629ecf8d44e9d10b323a317b2e424a17 *man/radioButtons.Rd
+c837729b4fc389cd68abc41bc254053c *man/reactive.Rd
+4734037e317cbe3fe6aab349f1802a22 *man/reactiveFileReader.Rd
 1233e22b239fff94341d8a8c2b3d7f4f *man/reactivePlot.Rd
-b94f81d4263113ca5f4bcd300205d5ae *man/reactivePoll.Rd
+8430f33d78d7ed9bb11107996137c100 *man/reactivePoll.Rd
 ba03335e953884bb60bce143cff17a84 *man/reactivePrint.Rd
 69ba7fcda401f11bb6a1297d9cbb31a5 *man/reactiveTable.Rd
 b09c00b93df747fe2e0e63ba438d68e9 *man/reactiveText.Rd
-9e281f456e4e2acaabc762c13ab5f3d2 *man/reactiveTimer.Rd
+c40a6528c40e5b36cb775e783f1fe6d7 *man/reactiveTimer.Rd
 3c02e74514e9a44e0b6a06fb21ef7c02 *man/reactiveUI.Rd
 c84a1d66e76e48ccb58d2ba45abcdb74 *man/reactiveValues.Rd
-c7ed8d593149e3fca5c233605a609166 *man/reactiveValuesToList.Rd
+cdd30631426dd57b2ec76e574b3fac15 *man/reactiveValuesToList.Rd
 d1197c9ae61dea32d0c572b007a6d977 *man/registerInputHandler.Rd
 5acd414830d166e09a322fb79abe8b8d *man/removeInputHandler.Rd
-03cb92baf43aa0e922b5b678f1a51ced *man/renderDataTable.Rd
-1c6bcdebb4c55896128f7de9afbd9ea0 *man/renderImage.Rd
-526999e45257a86e8b0684b97582f8d5 *man/renderPlot.Rd
-be68b5e285b6c09990f4ec5c7f77ba00 *man/renderPrint.Rd
-16681ee63f9c675e1fd7fcdc83d4f154 *man/renderTable.Rd
-9a07c4f1afd5d0f84f611d04d301aa34 *man/renderText.Rd
-92dd06efa119caadc24323c23a7e34d3 *man/renderUI.Rd
-4530f4e07dd72ba8b85f255131298d32 *man/repeatable.Rd
-a11160011ea5760584389c932ac46026 *man/req.Rd
+e165d130c98a3a604eab4bdf2fece4c3 *man/removeUI.Rd
+481e593983a4b12b58999b9edc701e5d *man/renderDataTable.Rd
+68a42ab42b758dd231c13b45e401c7c0 *man/renderImage.Rd
+56df2054d0d46dbdcd21871890b4f942 *man/renderPlot.Rd
+5033ea616a18927a45a3624e4c0d9e87 *man/renderPrint.Rd
+a7f657cc7de1d2b53391731fb4c1701a *man/renderTable.Rd
+9713fe1dff93b89d8400a76528373143 *man/renderText.Rd
+cb63e3d50cf01f80de1b720febe4e32b *man/renderUI.Rd
+28c14b86847bad36ea6b4a01b0a9d314 *man/repeatable.Rd
+696d09d4d1eb49a609b25e5cc79675bc *man/req.Rd
+f0670deffbe1f77380892ca1f5a8b37e *man/restoreInput.Rd
 d47f1a9e121c4bd2bb41034c8b96d50f *man/runApp.Rd
 6e500ae957bc388d27e14b5bc1851b68 *man/runExample.Rd
-54e26bdc1dbeef84fafd8151362eb5f5 *man/runGadget.Rd
-7afc122fd0c9959347bd89d6569967b2 *man/runUrl.Rd
-889674871143088dca247af109aba853 *man/selectInput.Rd
+1524263164e53933bc07e3eb968a113a *man/runGadget.Rd
+a26b63c9973d07c18281d758bfac1d28 *man/runUrl.Rd
+fc0e60e0d97dcc6fca79678b8b3ef925 *man/safeError.Rd
+68b05175fa88a32c763810437d72417b *man/selectInput.Rd
 312354ec69a77cc6f25c682811203170 *man/serverInfo.Rd
-c0fc417b91f41e3fc749a8438a672664 *man/session.Rd
-0e3da97562620d80f8fec5922c140051 *man/shiny-options.Rd
+c540d7b2961aa2a6eb3738244643fc33 *man/session.Rd
+2da0c9c542da2c542c440713e9171468 *man/setBookmarkExclude.Rd
+1a4118e80bcdf4086c793bcf91ca2a86 *man/shiny-options.Rd
 30cc9e53fee6282852effbdad93a696e *man/shiny-package.Rd
-f393697883a3dd9a8b916d10c36c2715 *man/shinyApp.Rd
+56f59d6daedc6e547314478d45918c78 *man/shinyApp.Rd
 d5ec7a3b8a327bcd55172015ed341e81 *man/shinyDeprecated.Rd
-f6886c08ce00f32eaac58780fb43845b *man/shinyServer.Rd
+20198c4520f4383415916da04d79d716 *man/shinyOptions.Rd
+0aaa8b2fd73d0f2505700a4433435e65 *man/shinyServer.Rd
 5afad6e82d6fcba704801a058d7b2df9 *man/shinyUI.Rd
-9a99e4c5493e4dc37d2ea360c7c47d22 *man/showReactLog.Rd
-40ed432d6dcadbdc13b489bf1541fb56 *man/sidebarLayout.Rd
+12f2e5120721a796286b3839d56a2294 *man/showBookmarkUrlModal.Rd
+5446bebae929fd941f5bd6cd7c0e5a22 *man/showModal.Rd
+c4d7c2453fbdabcf687b871e6af4b300 *man/showNotification.Rd
+0b835143915d5213dad9a0c0a2d03b52 *man/showReactLog.Rd
+095b29965d301213c8a481c898072b82 *man/sidebarLayout.Rd
 ed31f0a3d0e3457d9d3f1b22a6d1d4a7 *man/sidebarPanel.Rd
 025c15a6e3be95777dda20d2559a7497 *man/singleton.Rd
-820ff8e874cf17d27d645e8c74250e20 *man/sliderInput.Rd
-a5833f828be8650c9522162c191a33cc *man/splitLayout.Rd
+663fa0ff7955d113c0635dca8dd6e33b *man/sliderInput.Rd
+048dd9ea4438d2079ec417608bedd1d5 *man/splitLayout.Rd
 f1dbb30aeb7032894d8af89caddf1e8f *man/stacktrace.Rd
 029e949411bc4c0aa9658e32f7319aaa *man/stopApp.Rd
-4bff8e9c829c1c2ec51990ea2b688fe3 *man/submitButton.Rd
+e4d3181367e06d0d7a5d3ea8010facb7 *man/submitButton.Rd
 d32cc8ebf7ebce324a0983102a0a6de9 *man/suppressDependencies.Rd
 1b43b425f3cc300e5624e01f80802af7 *man/tabPanel.Rd
 f98f806c6a8d5837242934318e4c7d77 *man/tableOutput.Rd
-80acc40f235b630f195d3f7fc429eafd *man/tabsetPanel.Rd
+6fdfc632cfd96a91746d5cc168359b68 *man/tabsetPanel.Rd
 5c7c7daf31e2492fb5a21cfae8c0be1d *man/tag.Rd
-45023854d913b20af50a304a477a78c1 *man/textInput.Rd
+790855f0657c91551f3c288d256bed1a *man/textAreaInput.Rd
+f75162e96f0cb3df7bd385cb0b600332 *man/textInput.Rd
 f53a4f566d6a81d6b578f386684cc50d *man/textOutput.Rd
-bbe1be41a94ffbb97843451019097f38 *man/titlePanel.Rd
-9541911a966355b1499261d566c86a3a *man/updateCheckboxGroupInput.Rd
-c1b237fcacd6b702b2bf8c1607546e5c *man/updateCheckboxInput.Rd
-b822d95eee0a87eea130389d0b32ffdd *man/updateDateInput.Rd
-25ab344dbc62b51c459463438f54944d *man/updateDateRangeInput.Rd
-f0652ea5fd38ed77220c0d5aba09537a *man/updateNumericInput.Rd
-e3de87b467c53de59b8516f02eec0af9 *man/updateRadioButtons.Rd
-435e9fe16cc3d7819089efd18b2fd1d0 *man/updateSelectInput.Rd
-4ee2e93b2e60b19cab4a4156a5afc800 *man/updateSliderInput.Rd
-831e31de34e4eb86d5d6f6ac0621bbe3 *man/updateTabsetPanel.Rd
-9d516769d6ececfcfb9c812861018cb6 *man/updateTextInput.Rd
-59135b770d93de3b6cfb231e77401354 *man/validate.Rd
+01e84543a9c89efdcb047e79db9b0798 *man/titlePanel.Rd
+0c0bc2ae8bddcd03a94c277cbaadfae0 *man/updateActionButton.Rd
+1ad46a40db182925ea25b172a213343a *man/updateCheckboxGroupInput.Rd
+db1b0f13650b95f71793ae1bd094de40 *man/updateCheckboxInput.Rd
+0c356b3d03d1309dd111198731254df1 *man/updateDateInput.Rd
+2b93a7bf335d9c693a3fa323fb8bdaf0 *man/updateDateRangeInput.Rd
+6fc25fe24ace006d2d472e0dca0d7140 *man/updateNumericInput.Rd
+dd9d5a3e653c5eb24914fc0c87d5279e *man/updateQueryString.Rd
+ffb9ee1fbcd72ae2c05f28d248bf9808 *man/updateRadioButtons.Rd
+8aae9d3d8241ca089b73b3bdc82d3aea *man/updateSelectInput.Rd
+9a3a91b1779425416a020aa2face286f *man/updateSliderInput.Rd
+c3aa33fcc31510f39c9f08b003bd37df *man/updateTabsetPanel.Rd
+e6af9d2a830c2556201347e1c76f1b2a *man/updateTextAreaInput.Rd
+546a3d58223af5c48c287440c29013a0 *man/updateTextInput.Rd
+f4e84efeebbd28c7ef8646401ec5af6d *man/urlModal.Rd
+6d91e1b7bb98814c01bd90ed44ef9520 *man/validate.Rd
 21bdae51050d3832ab46550659947bd0 *man/validateCssUnit.Rd
 489c6b8cdebd85e26d36d0f893efae53 *man/verbatimTextOutput.Rd
-1a739c4f230f14065bbc1c4916069b7e *man/verticalLayout.Rd
+2b7dbd754c2b127a4c14019e27d904e5 *man/verticalLayout.Rd
 ca965f9478634d57819776471e7df8c6 *man/viewer.Rd
 29fc1b3faa440f561c6b78d02d6ccbe4 *man/wellPanel.Rd
 a12068140cb7a01bdf11ef6168e328f3 *man/withMathJax.Rd
-06d9ec1c35eebac64a4e0a1f2cecb4a3 *man/withProgress.Rd
+8cebd842703578396fe5df3681c91152 *man/withProgress.Rd
 0138d018b7594f66e96ec66f82763758 *man/withTags.Rd
-d500b135b90a2c15e6b1291e1864a29e *tests/test-all.R
+d44a15e0a188bccb87dcab4fefdc872e *tests/test-all.R
 89f3a210025c4dee6c10f4b357eefbbf *tests/test-encoding/01-symbols/app.R
 48c9ebbeff452be93d0250da3d099f38 *tests/test-encoding/02-backslash/server.R
 f83fbbf30d068acc35d1180c2f078c57 *tests/test-encoding/02-backslash/ui.R
 f58976e91aff9e28a61444ca5926dc57 *tests/test-encoding/test-all.R
-38a278f834081a0a0e64ec3e4737cdd9 *vignettes/events.Rmd
+02f05d1aa4c8bcb4808c39938deed480 *tests/testthat/helper.R
+12821fa94f20f5207fad48c25bb57178 *tests/testthat/test-bookmarking.R
+4657d2b5d67291cc64000b7213acb29e *tests/testthat/test-bootstrap.r
+3e5c74660dbf4c2d5e21961465c5c973 *tests/testthat/test-diagnostics.R
+dbe84e866824cfdf042f6d92729188cf *tests/testthat/test-gc.r
+8ae16d4481858309a4625678e2b8e040 *tests/testthat/test-input-handler.R
+f9ee5309105f8f0c9743572e7409336f *tests/testthat/test-modules.R
+f3f69a0a024170377ac09de1c788ba12 *tests/testthat/test-options.R
+093c126111fb2a656665464f4238b2cd *tests/testthat/test-plot-coordmap.R
+d05aec42cdf4064b0f409927812285cc *tests/testthat/test-reactivity.r
+bc84dab9f44f9079568a0c2e4fc83884 *tests/testthat/test-stack.R
+5c2dfe371178ff9274169cde1bc7e5f3 *tests/testthat/test-stacks.R
+fbace4e45a8036a85bdd983da02b330c *tests/testthat/test-staticdocs.R
+6269cd71b94d9dca02179dfa20aeea3b *tests/testthat/test-stop-app.R
+edce45a03c0b1ffe775546374c10aed0 *tests/testthat/test-text.R
+fe4b5516a083f1c9ede932e37b27e34c *tests/testthat/test-timer.R
+c59f5d981a17ad5c7e93f367abb45a1b *tests/testthat/test-ui.R
+77572f6e0036925f9c37a120d93beba7 *tests/testthat/test-update-input.R
+10bf6c1026eb046245839061ea91494b *tests/testthat/test-url.R
+b5978ec26571bd4116d061b90994a70d *tests/testthat/test-utils.R
diff --git a/NAMESPACE b/NAMESPACE
index 502df16..f088a36 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -11,6 +11,7 @@ S3method("[",shinyoutput)
 S3method("[<-",reactivevalues)
 S3method("[<-",shinyoutput)
 S3method("[[",reactivevalues)
+S3method("[[",session_proxy)
 S3method("[[",shinyoutput)
 S3method("[[<-",reactivevalues)
 S3method("[[<-",shinyoutput)
@@ -39,6 +40,7 @@ export(addResourcePath)
 export(animationOptions)
 export(as.shiny.appobj)
 export(basicPage)
+export(bookmarkButton)
 export(bootstrapLib)
 export(bootstrapPage)
 export(br)
@@ -65,7 +67,9 @@ export(downloadButton)
 export(downloadHandler)
 export(downloadLink)
 export(em)
+export(enableBookmarking)
 export(eventReactive)
+export(exportTestValues)
 export(exprToFunction)
 export(extractStackTrace)
 export(fileInput)
@@ -79,7 +83,9 @@ export(flowLayout)
 export(fluidPage)
 export(fluidRow)
 export(formatStackTrace)
+export(freezeReactiveValue)
 export(getDefaultReactiveDomain)
+export(getShinyOption)
 export(h1)
 export(h2)
 export(h3)
@@ -102,12 +108,14 @@ export(includeMarkdown)
 export(includeScript)
 export(includeText)
 export(inputPanel)
+export(insertUI)
 export(installExprFunction)
 export(invalidateLater)
 export(is.reactive)
 export(is.reactivevalues)
 export(is.shiny.appobj)
 export(is.singleton)
+export(isTruthy)
 export(isolate)
 export(knit_print.html)
 export(knit_print.reactive)
@@ -119,6 +127,8 @@ export(mainPanel)
 export(makeReactiveBinding)
 export(markRenderFunction)
 export(maskReactiveContext)
+export(modalButton)
+export(modalDialog)
 export(navbarMenu)
 export(navbarPage)
 export(navlistPanel)
@@ -128,7 +138,14 @@ export(ns.sep)
 export(numericInput)
 export(observe)
 export(observeEvent)
+export(onBookmark)
+export(onBookmarked)
+export(onFlush)
+export(onFlushed)
 export(onReactiveDomainEnded)
+export(onRestore)
+export(onRestored)
+export(onSessionEnded)
 export(outputOptions)
 export(p)
 export(pageWithSidebar)
@@ -154,6 +171,9 @@ export(reactiveValues)
 export(reactiveValuesToList)
 export(registerInputHandler)
 export(removeInputHandler)
+export(removeModal)
+export(removeNotification)
+export(removeUI)
 export(renderDataTable)
 export(renderImage)
 export(renderPlot)
@@ -163,21 +183,28 @@ export(renderText)
 export(renderUI)
 export(repeatable)
 export(req)
+export(restoreInput)
 export(runApp)
 export(runExample)
 export(runGadget)
 export(runGist)
 export(runGitHub)
 export(runUrl)
+export(safeError)
 export(selectInput)
 export(selectizeInput)
 export(serverInfo)
+export(setBookmarkExclude)
 export(setProgress)
 export(shinyApp)
 export(shinyAppDir)
 export(shinyAppFile)
+export(shinyOptions)
 export(shinyServer)
 export(shinyUI)
+export(showBookmarkUrlModal)
+export(showModal)
+export(showNotification)
 export(showReactLog)
 export(sidebarLayout)
 export(sidebarPanel)
@@ -199,10 +226,12 @@ export(tagAppendChildren)
 export(tagList)
 export(tagSetChildren)
 export(tags)
+export(textAreaInput)
 export(textInput)
 export(textOutput)
 export(titlePanel)
 export(uiOutput)
+export(updateActionButton)
 export(updateCheckboxGroupInput)
 export(updateCheckboxInput)
 export(updateDateInput)
@@ -210,12 +239,15 @@ export(updateDateRangeInput)
 export(updateNavbarPage)
 export(updateNavlistPanel)
 export(updateNumericInput)
+export(updateQueryString)
 export(updateRadioButtons)
 export(updateSelectInput)
 export(updateSelectizeInput)
 export(updateSliderInput)
 export(updateTabsetPanel)
+export(updateTextAreaInput)
 export(updateTextInput)
+export(urlModal)
 export(validate)
 export(validateCssUnit)
 export(verbatimTextOutput)
diff --git a/NEWS b/NEWS
deleted file mode 100644
index a3d56ae..0000000
--- a/NEWS
+++ /dev/null
@@ -1,970 +0,0 @@
-shiny 0.13.2
---------------------------------------------------------------------------------
-
-* Updated documentation for `htmlTemplate`.
-
-shiny 0.13.1
---------------------------------------------------------------------------------
-
-* `flexCol` did not work on RStudio for Windows or Linux.
-
-* Fixed #561: DataTables might pop up a warning when the data is updated
-  extremely frequently.
-
-* Fixed RStudio debugger integration.
-
-* BREAKING CHANGE: The long-deprecated ability to pass functions (rather than
-  expressions) to reactive() and observe() has finally been removed.
-
-shiny 0.13.0
---------------------------------------------------------------------------------
-
-* Fixed #962: plot interactions did not work with the development version of
-  ggplot2 (after ggplot2 1.0.1).
-
-* Fixed #902: the `drag_drop` plugin of the selectize input did not work.
-
-* Fixed #933: `updateSliderInput()` does not work when only the label is
-  updated.
-
-* Multiple imageOutput/plotOutput calls can now share the same brush id. Shiny
-  will ensure that performing a brush operation will clear any other brush with
-  the same id.
-
-* Added `placeholder` option to `textInput`.
-
-* Improved support for Unicode characters on Windows (#968).
-
-* Fixed bug in `selectInput` and `selectizeInput` where values with double
-  quotes were not properly escaped.
-
-* `runApp()` can now take a path to any .R file that yields a `shinyApp` object;
-  previously, the path had to be a directory that contained an app.R file (or
-  server.R if using separately defined server and UI). Similarly, introduced
-  `shinyAppFile()` function which creates a `shinyApp` object for an .R file
-  path, just as `shinyAppDir()` does for a directory path.
-
-* Introduced Shiny Modules, which are designed to 1) simplify the reuse of
-  Shiny UI/server logic and 2) make authoring and maintaining complex Shiny
-  apps much easier. See the article linked from `?callModule`.
-
-* `invalidateLater` and `reactiveTimer` no longer require an explicit `session`
-  argument; the default value uses the current session.
-
-* Added `session$reload()` method, the equivalent of hitting the browser's
-  Reload button.
-
-* Added `shiny.autoreload` option, which will automatically cause browsers to
-  reload whenever Shiny app files change on disk. This is intended to shorten
-  the feedback cycle when tweaking UI code.
-
-* Errors are now printed with stack traces! This should make it tremendously
-  easier to track down the causes of errors in Shiny. Try it by calling
-  `stop("message")` from within an output, reactive, or observer. Shiny itself
-  adds a lot of noise to the call stack, so by default, it attempts to hide all
-  but the relevant levels of the call stack. You can turn off this behavior by
-  setting `options(shiny.fullstacktrace=TRUE)` before or during app startup.
-
-* Fixed #1018: the selected value of a selectize input is guaranteed to be
-  selected in server-side mode.
-
-* Added `req` function, which provides a simple way to prevent a reactive,
-  observer, or output from executing until all required inputs and values are
-  available. (Similar functionality has been available for a while using
-  validate/need, but req provides a much simpler and more direct interface.)
-
-* Improve stability with Shiny Server when many subapps are used, by deferring
-  the loading of subapp iframes until a connection has first been established
-  with the server.
-
-* Upgrade to Font Awesome 4.5.0.
-
-* Upgraded to Bootstrap 3.3.5.
-
-* Switched to an almost-complete build of jQuery UI with the exception of the
-  datepicker extension, which conflicts with Shiny's date picker.
-
-* Added `fillPage` function, an alternative to `fluidPage`, `fixedPage`, etc.
-  that is designed for apps that fill the entire available page width/height.
-
-* Added `fillRow` and `fillCol` functions, for laying out proportional grids in
-  `fillPage`. For modern browsers only.
-
-* Added `runGadget`, `paneViewer`, `dialogViewer`, and `browserViewer`
-  functions to support Shiny Gadgets. More detailed docs about gadgets coming
-  soon.
-
-* Added support for the new htmltools 0.3 feature `htmlTemplate`. It's now
-  possible to use regular HTML markup to design your UI, but still use R
-  expressions to define inputs, outputs, and HTML widgets.
-
-shiny 0.12.2
---------------------------------------------------------------------------------
-
-* GitHub changed URLs for gists from .tar.gz to .zip, so `runGist` was updated
-  to work with the new URLs.
-
-* Callbacks from the session object are now guaranteed to execute in the order
-  in which registration occurred.
-
-* Minor bugs in sliderInput's animation behavior have been fixed. (#852)
-
-* Updated to ion.rangeSlider to 2.0.12.
-
-* Added `shiny.minified` option, which controls whether the minified version
-  of shiny.js is used. Setting it to FALSe can be useful for debugging. (#826,
-  #850)
-
-* Fixed an issue for outputting plots from ggplot objects which also have an
-  additional class whose print method takes precedence over `print.ggplot`.
-  (#840, 841)
-
-* Added `width` option to Shiny's input functions. (#589, #834)
-
-* Added two alias functions of `updateTabsetPanel()` to update the selected tab:
-  `updateNavbarPage()` and `updateNavlistPanel()`. (#881)
-
-* All non-base functions are now explicitly namespaced, to pass checks in
-  R-devel.
-
-* Shiny now correctly handles HTTP HEAD requests. (#876)
-
-shiny 0.12.1
---------------------------------------------------------------------------------
-
-* Fixed an issue where unbindAll() causes subsequent bindAll() to be ignored for
-  previously bound outputs. (#856)
-
-* Undeprecate `dataTableOutput` and `renderDataTable`, which had been deprecated
-  in favor of the new DT package. The DT package is a bit too new and has a
-  slightly different API, we were too hasty in deprecating the existing Shiny
-  functions.
-
-shiny 0.12.0
---------------------------------------------------------------------------------
-
-* Switched from RJSONIO to jsonlite. This improves consistency and speed when
-  converting between R data structures and JSON. One notable change is that
-  POSIXt objects are now serialized to JSON in UTC8601 format (like
-  "2015-03-20T20:00:00Z"), instead of as seconds from the epoch).
-
-* In addition to the existing support for clicking and hovering on plots
-  created by base graphics, added support for double-clicking and brushing.
-  (#769)
-
-* Added support for clicking, hovering, double-clicking, and brushing for
-  plots created by ggplot2, including support for facets. (#802)
-
-* Added `nearPoints` and `brushedPoints` functions for easily selecting rows of
-  data that are clicked/hovered, or brushed. (#802)
-
-* Added `shiny.port` option. If this is option is set, `runApp()` will listen on
-  this port by default. (#756)
-
-* `runUrl`, `runGist`, and `runGitHub` now can save downloaded applications,
-  with the `destdir` argument. (#688)
-
-* Restored ability to set labels for `selectInput`. (#741)
-
-* Travis continuous integration now uses Travis's native R support.
-
-* Fixed encoding issue when the server receives data from the client browser.
-  (#742)
-
-* The `session` object now has class `ShinySession`, making it easier to test
-  whether an object is indeed a session object. (#720, #746)
-
-* Fix JavaScript error when an output appears in nested uiOutputs. (Thanks,
-  Gregory Zhang. #749)
-
-* Eliminate delay on receiving new value when `updateSliderInput(value=...)` is
-  called.
-
-* Updated to DataTables (Javascript library) 1.10.5.
-
-* Fixed downloading of files that have no filename extension. (#575, #753)
-
-* Fixed bug where nested UI outputs broke outputs. (#749, #750)
-
-* Removed unneeded HTML ID attributes for `checkboxGroupInputs` and
-  `radioButtons`. (#684)
-
-* Fixed bug where checkboxes were still active even after `Shiny.unbindAll()`
-  was called. (#206)
-
-* The server side selectize input will load the first 1000 options by default
-  before users start to type and search in the box. (#823)
-
-* renderDataTable() and dataTableOutput() have been deprecated in shiny and will
-  be removed in future versions of shiny. Please use the DT package instead:
-  http://rstudio.github.io/DT/ (#807)
-
-shiny 0.11.1
---------------------------------------------------------------------------------
-
-* Major client-side performance improvements for pages that have many
-  conditionalPanels, tabPanels, and plotOutputs. (#693, #717, #723)
-
-* `tabPanel`s now use the `title` for `value` by default. This fixes a bug
-  in which an icon in the title caused problems with a conditionalPanel's test
-  condition. (#725, #728)
-
-* `selectInput` now has a `size` argument to control the height of the input
-  box. (#729)
-
-* `navbarPage` no longer includes a first row of extra whitespace when
-  `header=NULL`. (#722)
-
-* `selectInput`s now use Bootstrap styling when `selectize=FALSE`. (#724)
-
-* Better vertical spacing of label for checkbox groups and radio buttons.
-
-* `selectInput` correctly uses width for both selectize and non-selectize
-  inputs. (#702)
-
-* The wrapper tag generated by `htmlOutput` and `uiOutput` can now be any type
-  of HTML tag, instead of just span and div. Also, custom classes are now
-  allowed on the tag. (#704)
-
-* Slider problems in IE 11 and Chrome on touchscreen-equipped Windows computers
-  have been fixed. (#700)
-
-* Sliders now work correctly with draggable panels. (#711)
-
-* Fixed arguments in `fixedPanel`. (#709)
-
-* downloadHandler content callback functions are now invoked with a temp file
-  name that has the same extension as the final filename that will be used by
-  the download. This is to deal with the fact that some file writing functions
-  in R will auto-append the extension for their file type (pdf, zip).
-
-shiny 0.11
---------------------------------------------------------------------------------
-
-* Changed sliders from jquery-slider to ion.rangeSlider. These sliders have
-  an improved appearance, support updating more properties from the server,
-  and can be controlled with keyboard input.
-
-* Switched from Bootstrap 2 to Bootstrap 3. For most users, this will work
-  seamlessly, but some users may need to use the shinybootstrap2 package for
-  backward compatibility.
-
-* The UI of a Shiny app can now have a body tag. This is useful for CSS
-  templates that use classes on the body tag.
-
-* `actionButton` and `actionLink` now pass their `...` arguments to the
-  underlying tag function. (#607)
-
-* Added `observeEvent` and `eventReactive` functions for clearer, more concise
-  handling of `actionButton`, plot clicks, and other naturally-imperative
-  inputs.
-
-* Errors that happen in reactives no longer prevent any remaining pending
-  observers from executing. It is also now possible for users to control how
-  errors are handled, with the 'shiny.observer.error' global option. (#603,
-  #604)
-
-* Added an `escape` argument to `renderDataTable()` to escape the HTML entities
-  in the data table for security reasons. This might break tables from previous
-  versions of shiny that use raw HTML in the table content, and the old behavior
-  can be brought back by `escape = FALSE` if you are aware of the security
-  implications. (#627)
-
-* Changed the URI encoding/decoding functions internally to use `encodeURI()`,
-  `encodeURIComponent()`, and `decodeURIComponent()` from the httpuv package
-  instead of `utils::URLencode()` and `utils::URLdecode()`. (#630)
-
-* Shiny's web assets are now minified.
-
-* The default reactive domain is now available in event handler functions. (#669)
-
-* Password input fields can now be used, with `passwordInput()`. (#672)
-
-shiny 0.10.2.2
---------------------------------------------------------------------------------
-
-* Remove use of `rstudio::viewer` in a code example, for R CMD check.
-
-shiny 0.10.2.1
---------------------------------------------------------------------------------
-
-* Changed some examples to use \donttest instead of \dontrun.
-
-shiny 0.10.2
---------------------------------------------------------------------------------
-
-* The minimal version of R required for the shiny package is 3.0.0 now.
-
-* Shiny apps can now consist of a single file, app.R, instead of ui.R and
-  server.R.
-
-* Upgraded DataTables from 1.9.4 to 1.10.2. This might be a breaking change if
-  you have customized the DataTables options in your apps. (More info:
-  https://github.com/rstudio/shiny/pull/558)
-
-* File uploading via `fileInput()` works for Internet Explorer 8 and 9 now. Note
-  IE8/9 do not support multiple files from a single file input. If you need to
-  upload multiple files, you have to use one file input for each file.
-
-* Switched away from reference classes to R6.
-
-* Reactive log performance has been greatly improved.
-
-* Added `Progress` and `withProgress`, to display the progress of computation
-  on the client browser.
-
-* Fixed #557: updateSelectizeInput(choices, server = TRUE) did not work when
-  `choices` is a character vector.
-
-* Searching in DataTables is case-insensitive and the search strings are not
-  treated as regular expressions by default now. If you want case-sensitive
-  searching or regular expressions, you can use the configuration options
-  `search$caseInsensitive` and `search$regex`, e.g. `renderDataTable(...,
-  options = list(search = list(caseInsensitve = FALSE, regex = TRUE)))`.
-
-* Added support for `htmltools::htmlDependency`'s new `attachment` parameter to
-  `renderUI`/`uiOutput`.
-
-* Exported `createWebDependency`. It takes an `htmltools::htmlDependency` object
-  and makes it available over Shiny's built-in web server.
-
-* Custom output bindings can now render `htmltools::htmlDependency` objects at
-  runtime using `Shiny.renderDependencies()`.
-
-* Fixes to rounding behavior of sliderInput. (#301, #502)
-
-* Updated selectize.js to version 0.11.2. (#596)
-* Added `position` parameter to `navbarPage`.
-
-shiny 0.10.1
---------------------------------------------------------------------------------
-
-* Added Unicode support for Windows. Shiny apps running on Windows must use the
-  UTF-8 encoding for ui.R and server.R (also the optional global.R) if they
-  contain non-ASCII characters. See this article for details and examples:
-  http://shiny.rstudio.com/gallery/unicode-characters.html (#516)
-
-* `runGitHub()` also allows the 'username/repo' syntax now, which is equivalent
-  to `runGitHub('repo', 'username')`. (#427)
-
-* `navbarPage()` now accepts a `windowTitle` parameter to set the web browser
-  page title to something other than the title displayed in the navbar.
-
-* Added an `inline` argument to `textOutput()`, `imageOutput()`, `plotOutput()`,
-  and `htmlOutput()`. When `inline = TRUE`, these outputs will be put in
-  `span()` instead of the default `div()`. This occurs automatically when these
-  outputs are created via the inline expressions (e.g. `r renderText(expr)`) in
-  R Markdown documents. See an R Markdown example at
-  http://shiny.rstudio.com/gallery/inline-output.html (#512)
-
-* Added support for option groups in the select/selectize inputs. When the
-  `choices` argument for `selectInput()`/`selectizeInput()` is a list of
-  sub-lists and any sub-list is of length greater than 1, the HTML tag
-  `<optgroup>` will be used. See an example at
-  http://shiny.rstudio.com/gallery/option-groups-for-selectize-input.html (#542)
-
-shiny 0.10.0
---------------------------------------------------------------------------------
-
-* BREAKING CHANGE: By default, observers now terminate themselves if they were
-  created during a session and that session ends. See ?domains for more details.
-
-* Shiny can now be used in R Markdown v2 documents, to create "Shiny Docs":
-  reports and presentations that combine narrative, statically computed output,
-  and fully dynamic inputs and outputs. For more info, including examples, see
-  http://rmarkdown.rstudio.com/authoring_shiny.html.
-
-* The `session` object that can be passed into a server function (e.g.
-  shinyServer(function(input, output, session) {...})) is now documented: see
-  `?session`.
-
-* Most inputs can now accept `NULL` label values to omit the label altogether.
-
-* New `actionLink` input control; like `actionButton`, but with the appearance
-  of a normal link.
-
-* `renderPlot` now calls `print` on its result if it's visible (i.e. no more
-  explicit `print()` required for ggplot2).
-
-* Introduced Shiny app objects (see `?shinyApp`). These essentially replace the
-  little-advertised ability for `runApp` to take a `list(ui=..., server=...)`
-  as the first argument instead of a directory (though that ability remains for
-  backward compatibility). Unlike those lists, Shiny app objects are tagged with
-  class `shiny.appobj` so they can be run simply by printing them.
-
-* Added `maskReactiveContext` function. It blocks the current reactive context,
-  to evaluate expressions that shouldn't use reactive sources directly. (This
-  should not be commonly needed.)
-
-* Added `flowLayout`, `splitLayout`, and `inputPanel` functions for putting UI
-  elements side by side. `flowLayout` lays out its children in a left-to-right,
-  top-to-bottom arrangement. `splitLayout` evenly divides its horizontal space
-  among its children (or unevenly divides if `cellWidths` argument is provided).
-  `inputPanel` is like `flowPanel`, but with a light grey background, and is
-  intended to be used to encapsulate small input controls wherever vertical
-  space is at a premium.
-
-* Added `serverInfo` to obtain info about the Shiny Server if the app is served
-  through it.
-
-* Added an `inline` argument (TRUE/FALSE) in `checkboxGroupInput()` and
-  `radioButtons()` to allow the horizontal layout (inline = TRUE) of checkboxes
-  or radio buttons. (Thanks, @saurfang, #481)
-
-* `sliderInput` and `selectizeInput`/`selectInput` now use a standard horizontal
-  size instead of filling up all available horizontal space. Pass `width="100%"`
-  explicitly for the old behavior.
-
-* Added the `updateSelectizeInput()` function to make it possible to process
-  searching on the server side (i.e. using R), which can be much faster than the
-  client side processing (i.e. using HTML and JavaScript). See the article at
-  http://shiny.rstudio.com/articles/selectize.html for a detailed introduction.
-
-* Fixed a bug of renderDataTable() when the data object only has 1 row and 1
-  column. (Thanks, ZJ Dai, #429)
-
-* `renderPrint` gained a new argument 'width' to control the width of the text
-  output, e.g. renderPrint({mtcars}, width = 40).
-
-* Fixed #220: the zip file for a directory created by some programs may not have
-  the directory name as its first entry, in which case runUrl() can fail. (#220)
-
-* `runGitHub()` can also take a value of the form "username/repo" in its first
-  argument, e.g. both runGitHub("shiny_example", "rstudio") and
-  runGitHub("rstudio/shiny_example") are valid ways to run the GitHub repo.
-
-shiny 0.9.1
---------------------------------------------------------------------------------
-
-* Fixed warning 'Error in Context$new : could not find function "loadMethod"'
-  that was happening to dependent packages on "R CMD check".
-
-shiny 0.9.0
---------------------------------------------------------------------------------
-
-* BREAKING CHANGE: Added a `host` parameter to runApp() and runExample(),
-  which defaults to the shiny.host option if it is non-NULL, or "127.0.0.1"
-  otherwise. This means that by default, Shiny applications can only be
-  accessed on the same machine from which they are served. To allow other
-  clients to connect, as in previous versions of Shiny, use "0.0.0.0"
-  (or the IP address of one of your network interfaces, if you care to be
-  explicit about it).
-
-* Added a new function `selectizeInput()` to use the JavaScript library
-  selectize.js (https://github.com/brianreavis/selectize.js), which extends
-  the basic select input in many aspects.
-
-* The `selectInput()` function also gained a new argument `selectize = TRUE`
-  to makes use of selectize.js by default. If you want to revert back to the
-  original select input, you have to call selectInput(..., selectize = FALSE).
-
-* Added Showcase mode, which displays the R code for an app right in the app
-  itself. You can invoke Showcase mode by passing `display.mode="showcase"`
-  to the `runApp()` function. Or, if an app is designed to run in Showcase
-  mode by default, add a DESCRIPTION file in the app dir with Title, Author,
-  and License fields; with "Type: Shiny"; and with "DisplayMode: Showcase".
-
-* Upgraded to Bootstrap 2.3.2 and jQuery 1.11.0.
-
-* Make `tags$head()` and `singleton()` behave correctly when used with
-  `renderUI()` and `uiOutput()`. Previously, "hoisting content to the head"
-  and "only rendering items a single time" were features that worked only
-  when the page was initially loading, not in dynamic rendering.
-
-* Files are now sourced with the `keep.source` option, to help with debugging
-  and profiling.
-
-* Support user-defined input parsers for data coming in from JavaScript using
-  the parseShinyInput method.
-
-* Fixed the bug #299: renderDataTable() can deal with 0-row data frames now.
-  (reported by Harlan Harris)
-
-* Added `navbarPage()` and `navbarMenu()` functions to create applications
-  with multiple top level panels.
-
-* Added `navlistPanel()` function to create layouts with a a bootstrap
-  navlist on the left and tabPanels on the right
-
-* Added `type` parameter to `tabsetPanel()` to enable the use of pill
-  style tabs in addition to the standard ones.
-
-* Added `position` paramter to `tabsetPanel()` to enable positioning of tabs
-  above, below, left, or right of tab content.
-
-* Added `fluidPage()` and `fixedPage()` functions as well as related row and
-  column layout functions for creating arbitrary bootstrap grid layouts.
-
-* Added `hr()` builder function for creating horizontal rules.
-
-* Automatically concatenate duplicate attributes in tag definitions
-
-* Added `responsive` parameter to page building functions for opting-out of
-  bootstrap responsive css.
-
-* Added `theme` parameter to page building functions for specifying alternate
-  bootstrap css styles.
-
-* Added `icon()` function for embedding icons from the
-  [font awesome](http://fontawesome.io/) icon library
-
-* Added `makeReactiveBinding` function to turn a "regular" variable into a
-  reactive one (i.e. reading the variable makes the current reactive context
-  dependent on it, and setting the variable is a source of reactivity).
-
-* Added a function `withMathJax()` to include the MathJax library in an app.
-
-* The argument `selected` in checkboxGroupInput(), selectInput(), and
-  radioButtons() refers to the value(s) instead of the name(s) of the
-  argument `choices` now. For example, the value of the `selected` argument
-  in selectInput(..., choices = c('Label 1' = 'x1', 'Label 2' = 'x2'),
-  selected = 'Label 2') must be updated to 'x2', although names/labels will
-  be automatically converted to values internally for backward
-  compatibility. The same change applies to updateCheckboxGroupInput(),
-  updateSelectInput(), and updateRadioButtons() as well. (#340)
-
-* Now it is possible to only update the value of a checkbox group, select input,
-  or radio buttons using the `selected` argument without providing the
-  `choices` argument in updateCheckboxGroupInput(), updateSelectInput(), and
-  updateRadioButtons(), respectively. (#340)
-
-* Added `absolutePanel` and `fixedPanel` functions for creating absolute-
-  and fixed-position panels. They can be easily made user-draggable by
-  specifying `draggable = TRUE`.
-
-* For the `options` argument of the function `renderDataTable()`, we can
-  pass literal JavaScript code to the DataTables library via `I()`. This
-  makes it possible to use any JavaScript object in the options, e.g. a
-  JavaScript function (which is not supported in JSON). See
-  `?renderDataTable` for details and examples.
-
-* DataTables also works under IE8 now.
-
-* Fixed a bug in DataTables pagination when searching is turned on, which
-  caused failures for matrices as well as empty rows when displaying data
-  frames using renderDataTable().
-
-* The `options` argument in `renderDataTable()` can also take a function
-  that returns a list. This makes it possible to use reactive values in the
-  options. (#392)
-
-* `renderDataTable()` respects more DataTables options now: (1) either
-  bPaginate = FALSE or iDisplayLength = -1 will disable pagination (i.e. all
-  rows are returned from the data); besides, this means we can also use -1
-  in the length menu, e.g. aLengthMenu = list(c(10, 30, -1), list(10, 30,
-  'All')); (2) we can disable searching for individual columns through the
-  bSearchable option, e.g. aoColumns = list(list(bSearchable = FALSE),
-  list(bSearchable = TRUE),...) (the search box for the first column is
-  hidden); (3) we can turn off searching entirely (for both global searching
-  and individual columns) using the option bFilter = FALSE.
-
-* Added an argument `callback` in `renderDataTable()` so that a custom
-  JavaScript function can be applied to the DataTable object. This makes it
-  much easier to use DataTables plug-ins.
-
-* For numeric columns in a DataTable, the search boxes support lower and
-  upper bounds now: a search query of the form "lower,upper" (without
-  quotes) indicates the limits [lower, upper]. For a column X, this means
-  the rows corresponding to X >= lower & X <= upper are returned. If we omit
-  either the lower limit or the upper limit, only the other limit will be
-  used, e.g. ",upper" means X <= upper.
-
-* `updateNumericInput(value)` tries to preserve numeric precision by avoiding
-  scientific notation when possible, e.g. 102145 is no longer rounded to
-  1.0214e+05 = 102140. (Thanks, Martin Loos. #401)
-
-* `sliderInput()` no longer treats a label wrapped in HTML() as plain text,
-  e.g. the label in sliderInput(..., label = HTML('<em>A Label</em>')) will
-  not be escaped any more. (#119)
-
-* Fixed #306: the trailing slash in a path could fail `addResourcePath()`
-  under Windows. (Thanks, ZJ Dai)
-
-* Dots are now legal characters for inputId/outputId. (Thanks, Kevin
-  Lindquist. #358)
-
-shiny 0.8.0
---------------------------------------------------------------------------------
-
-* Debug hooks are registered on all user-provided functions and (reactive)
-  expressions (e.g., in renderPlot()), which makes it possible to set
-  breakpoints in these functions using the latest version of the RStudio
-  IDE, and the RStudio visual debugging tools can be used to debug Shiny
-  apps. Internally, the registration is done via installExprFunction(),
-  which is a new function introduced in this version to replace
-  exprToFunction() so that the registration can be automatically done.
-
-* Added a new function renderDataTable() to display tables using the
-  JavaScript library DataTables. It includes basic features like pagination,
-  searching (global search or search by individual columns), sorting (by
-  single or multiple columns). All these features are implemented on the R
-  side; for example, we can use R regular expressions for searching.
-  Besides, it also uses the Bootstrap CSS style. See the full
-  documentation and examples in the tutorial:
-  http://rstudio.github.io/shiny/tutorial/#datatables
-
-* Added a new option `shiny.error` which can take a function as an error
-  handler. It is called when an error occurs in an app (in user-provided
-  code), e.g., after we set options(shiny.error = recover), we can enter a
-  specified environment in the call stack to debug our code after an error
-  occurs.
-
-* The argument `launch.browser` in runApp() can also be a function,
-  which takes the URL of the shiny app as its input value.
-
-* runApp() uses a random port between 3000 and 8000 instead of 8100 now. It
-  will try up to 20 ports in case certain ports are not available.
-
-* Fixed a bug for conditional panels: the value `input.id` in the condition
-  was not correctly retrieved when the input widget had a type, such as
-  numericInput(). (reported by Jason Bryer)
-
-* Fixed two bugs in plotOutput(); clickId and hoverId did not give correct
-  coordinates in Firefox, or when the axis limits of the plot were changed.
-  (reported by Chris Warth and Greg D)
-
-* The minimal required version for the httpuv package was increased to 1.2
-  (on CRAN now).
-
-
-shiny 0.7.0
---------------------------------------------------------------------------------
-
-* Stopped sending websocket subprotocol. This fixes a compatibility issue with
-  Google Chrome 30.
-
-* The `input` and `output` objects are now also accessible via `session$input`
-  and `session$output`.
-
-* Added click and hover events for static plots; see `?plotOutput` for details.
-
-* Added optional logging of the execution states of a reactive program, and
-  tools for visualizing the log data. To use, start a new R session and call
-  `options(shiny.reactlog=TRUE)`. Then launch a Shiny app and interact with it.
-  Press Ctrl+F3 (or for Mac, Cmd+F3) in the browser to launch an interactive
-  visualization of the reactivity that has occurred. See `?showReactLog` for
-  more information.
-
-* Added `includeScript()` and `includeCSS()` functions.
-
-* Reactive expressions now have class="reactive" attribute. Also added
-  `is.reactive()` and `is.reactivevalues()` functions.
-
-* New `stopApp()` function, which stops an app and returns a value to the caller
-  of `runApp()`.
-
-* Added the `shiny.usecairo` option, which can be used to tell Shiny not to use
-  Cairo for PNG output even when it is installed. (Defaults to `TRUE`.)
-
-* Speed increases for `selectInput()` and `radioButtons()`, and their
-  corresponding updater functions, for when they have many options.
-
-* Added `tagSetChildren()` and `tagAppendChildren()` functions.
-
-* The HTTP request object that created the websocket is now accessible from the
-  `session` object, as `session$request`. This is a Rook-like request
-  environment that can be used to access HTTP headers, among other things.
-  (Note: When running in a Shiny Server environment, the request will reflect
-  the proxy HTTP request that was made from the Shiny Server process to the R
-  process, not the request that was made from the web browser to Shiny Server.)
-
-* Fix `getComputedStyle` issue, for IE8 browser compatibility (#196). Note:
-  Shiny Server is still required for IE8/9 compatibility.
-
-* Add shiny.sharedSecret option, to require the HTTP header Shiny-Shared-Secret
-  to be set to the given value.
-
-shiny 0.6.0
---------------------------------------------------------------------------------
-
-* `tabsetPanel()` can be directed to start with a specific tab selected.
-
-* Fix bug where multiple file uploads with 3 or more files result in incorrect
-  data.
-
-* Add `withTags()` function.
-
-* Add dateInput and dateRangeInput.
-
-* `shinyServer()` now takes an optional `session` argument, which is used for
-  communication with the session object.
-
-* Add functions to update values of existing inputs on a page, instead of
-  replacing them entirely.
-
-* Allow listening on domain sockets.
-
-* Added `actionButton()` to Shiny.
-
-* The server can now send custom JSON messages to the client. On the client
-  side, functions can be registered to handle these messages.
-
-* Callbacks can be registered to be called at the end of a client session.
-
-* Add ability to set priority of observers and outputs. Each priority level
-  gets its own queue.
-
-* Fix bug where the presence of a submit button would prevent sending of
-  metadata until the button was clicked.
-
-* `reactiveTimer()` and `invalidateLater()` by default no longer invalidate
-  reactive objects after the client session has closed.
-
-* Shiny apps can be run without a server.r and ui.r file.
-
-shiny 0.5.0
---------------------------------------------------------------------------------
-
-* Switch from websockets package for handling websocket connections to httpuv.
-
-* New method for detecting hidden output objects. Instead of checking that
-  height and width are 0, it checks that the object or any ancestor in the DOM
-  has style display:none.
-
-* Add `clientData` reactive values object, which carries information about the
-  client. This includes the hidden status of output objects, height/width plot
-  output objects, and the URL of the browser.
-
-* Add `parseQueryString()` function.
-
-* Add `renderImage()` function for sending arbitrary image files to the client,
-  and its counterpart, `imageOutput()`.
-
-* Add support for high-resolution (Retina) displays.
-
-* Fix bug #55, where `renderTable()` would throw error with an empty data frame.
-
-shiny 0.4.1
---------------------------------------------------------------------------------
-
-* Fix bug where width and height weren't passed along properly from
-  `reactivePlot` to `renderPlot`.
-
-* Fix bug where infinite recursion would happen when `reactivePlot` was passed
-  a function for width or height.
-
-shiny 0.4.0
---------------------------------------------------------------------------------
-
-* Added suspend/resume capability to observers.
-
-* Output objects are automatically suspended when they are hidden on the user's
-  web browser.
-
-* `runGist()` accepts GitHub's new URL format, which includes the username.
-
-* `reactive()` and `observe()` now take expressions instead of functions.
-
-* `reactiveText()`, `reactivePlot()`, and so on, have been renamed to
-  `renderText()`, `renderPlot()`, etc.  They also now take expressions instead
-  of functions.
-
-* Fixed a bug where empty values in a numericInput were sent to the R process
-  as 0. They are now sent as NA.
-
-shiny 0.3.1
---------------------------------------------------------------------------------
-
-* Fix issue #91: bug where downloading files did not work.
-
-* Add [[<- operator for shinyoutput object, making it possible to assign values
-  with `output[['plot1']] <- ...`.
-
-* Reactive functions now preserve the visible/invisible state of their returned
-  values.
-
-shiny 0.3.0
---------------------------------------------------------------------------------
-
-* Reactive functions are now evaluated lazily.
-
-* Add `reactiveValues()`.
-
-* Using `as.list()` to convert a reactivevalues object (like `input`) to a list
-  is deprecated. The new function `reactiveValuesToList()` should be used
-  instead.
-
-* Add `isolate()`. This function is used for accessing reactive functions,
-  without them invalidating their parent contexts.
-
-* Fix issue #58: bug where reactive functions are not re-run when all items in
-  a checkboxGroup are unchecked.
-
-* Fix issue #71, where `reactiveTable()` would return blank if the first
-  element of a data frame was NA.
-
-* In `plotOutput`, better validation for CSS units when specifying width and
-  height.
-
-* `reactivePrint()` no longer displays invisible output.
-
-* `reactiveText()` no longer displays printed output, only the return value
-  from a function.
-
-* The `runGitHub()` and `runUrl()` functions have been added, for running
-  Shiny apps from GitHub repositories and zip/tar files at remote URLs.
-
-* Fix issue #64, where pressing Enter in a textbox would cause a form to
-  submit.
-
-shiny 0.2.4
---------------------------------------------------------------------------------
-
-* `runGist` has been updated to use the new download URLs from
-  https://gist.github.com.
-
-* Shiny now uses `CairoPNG()` for output, when the Cairo package is available.
-  This provides better-looking output on Linux and Windows.
-
-shiny 0.2.3
---------------------------------------------------------------------------------
-
-* Ignore request variables for routing purposes
-
-shiny 0.2.2
---------------------------------------------------------------------------------
-
-* Fix CRAN warning (assigning to global environment)
-
-
-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 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
-
-
-shiny 0.1.2
---------------------------------------------------------------------------------
-
-Initial private beta release!
diff --git a/NEWS.md b/NEWS.md
new file mode 100644
index 0000000..c239736
--- /dev/null
+++ b/NEWS.md
@@ -0,0 +1,1032 @@
+shiny 0.14.2
+============
+
+This is a maintenance release of Shiny, with some bug fixes and minor new features.
+
+## Full changelog
+
+### Minor new features and improvements
+
+* Added a `fade` argument to `modalDialog()` -- setting it to `FALSE` will remove the usual fade-in animation for that modal window. ([#1414](https://github.com/rstudio/shiny/pull/1414))
+
+* Fixed a "duplicate binding" error that occurred in some edge cases involving `insertUI` and nested `uiOutput`. ([#1402](https://github.com/rstudio/shiny/pull/1402))
+
+* Fixed [#1422](https://github.com/rstudio/shiny/issues/1422): When using the `shiny.trace` option, allow specifying to only log SEND or RECV messages, or both. (PR [#1428](https://github.com/rstudio/shiny/pull/1428))
+
+* Fixed [#1419](https://github.com/rstudio/shiny/issues/1419): Allow overriding a JS custom message handler. (PR [#1445](https://github.com/rstudio/shiny/pull/1445))
+
+* Added `exportTestValues()` function, which allows a test driver to query the session for values internal to an application's server function. This only has an effect if the `shiny.testmode` option is set to `TRUE`. ([#1436](https://github.com/rstudio/shiny/pull/1436))
+
+### Bug fixes
+
+* Fixed [#1427](https://github.com/rstudio/shiny/issues/1427): make sure that modals do not close incorrectly when an element inside them is triggered as hidden. ([#1430](https://github.com/rstudio/shiny/pull/1430))
+
+* Fixed [#1404](https://github.com/rstudio/shiny/issues/1404): stack trace tests were not compatible with the byte-code compiler in R-devel, which now tracks source references.
+
+* `sliderInputBinding.setValue()` now sends a slider's value immediately, instead of waiting for the usual 250ms debounce delay. ([#1429](https://github.com/rstudio/shiny/pull/1429))
+
+* Fixed a bug where, in versions of R before 3.2, Shiny applications could crash due to a bug in R's implementation of `list2env()`. ([#1446](https://github.com/rstudio/shiny/pull/1446))
+
+shiny 0.14.1
+============
+
+This is a maintenance release of Shiny, with some bug fixes and minor new features.
+
+## Full changelog
+
+### Minor new features and improvements
+
+* Restored file inputs are now copied on restore, so that the restored application can't modify the bookmarked file. ([#1370](https://github.com/rstudio/shiny/issues/1370))
+
+* Added support for plot interaction in the development version of ggplot2, 2.1.0.9000. Also added support for ggplot2 plots with `coord_flip()` (in the development version of ggplot2). ([hadley/ggplot2#1781](https://github.com/hadley/ggplot2/issues/1781), [#1392](https://github.com/rstudio/shiny/pull/1392))
+
+
+### Bug fixes
+
+* Fixed [#1093](https://github.com/rstudio/shiny/issues/1093) better: `updateRadioButtons()` and `updateCheckboxGroupInput()` were not working correctly if the choices were given as a numeric vector. This had been solved in [#1291](https://github.com/rstudio/shiny/pull/1291), but that introduced a different bug [#1396](https://github.com/rstudio/shiny/issues/1396) that this better fix avoids. ([#1370](https://github.com/rstudio/shiny/pull/1397))
+
+* Fixed [#1368](https://github.com/rstudio/shiny/issues/1368): If an app with a file input was bookmarked and restored, and then the restored app was bookmarked and restored (without uploading a new file), then it would fail to restore the file the second time. ([#1370](https://github.com/rstudio/shiny/pull/1370))
+
+* Fixed [#1369](https://github.com/rstudio/shiny/issues/1369): `sliderInput()` did not allow showing numbers without a thousands separator.
+
+* Fixed [#1346](https://github.com/rstudio/shiny/issues/1346) and [#1107](https://github.com/rstudio/shiny/issues/1107) : jQuery UI's datepicker conflicted with the bootstrap-datepicker used by Shiny's `dateInput()` and `dateRangeInput()`. ([#1374](https://github.com/rstudio/shiny/pull/1374))
+
+### Library updates
+
+* Updated to bootstrap-datepicker 1.6.4. ([#1218](https://github.com/rstudio/shiny/issues/1218), [#1374](https://github.com/rstudio/shiny/pull/1374))
+
+* Updated to jQuery UI 1.12.1. Previously, Shiny included a build of 1.11.4 which was missing the datepicker component due to a conflict with the bootstrap-datepicker used by Shiny's `dateInput()` and `dateRangeInput()`. ([#1374](https://github.com/rstudio/shiny/pull/1374))
+
+
+shiny 0.14
+==========
+
+A new Shiny release is upon us! There are many new exciting features, bug fixes, and library updates. We'll just highlight the most important changes here, but you can browse through the full changelog below for details. This will likely be the last release before shiny 1.0, so get out your party hats!
+
+## Bookmarkable state
+
+Shiny now supports bookmarkable state: users can save the state of an application and get a URL which will restore the application with that state. There are two types of bookmarking: encoding the state in a URL, and saving the state to the server. With an encoded state, the entire state of the application is contained in the URL’s query string. You can see this in action with this app: https://gallery.shinyapps.io/113-bookmarking-url/. An example of a bookmark URL for this app is https: [...]
+
+**_Important note_:**
+> Saved-to-server bookmarking currently works with Shiny Server Open Source. Support on Shiny Server Pro, RStudio Connect, and shinyapps.io is under development and testing. However, URL-encoded bookmarking works on all hosting platforms.
+
+See [this article](http://shiny.rstudio.com/articles/bookmarking-state.html) to get started with bookmarkable state. There is also an [advanced-level article](http://shiny.rstudio.com/articles/advanced-bookmarking.html) (for apps that have a complex state), and [a modules article](http://shiny.rstudio.com/articles/bookmarking-modules.html) that details how to use bookmarking in conjunction with modules.
+
+## Notifications
+
+Shiny can now display notifications on the client browser by using the `showNotification()` function. Use [this demo app](https://gallery.shinyapps.io/116-notifications/) to play around with the notification API. Here's a screenshot of a very simple notification (shown when the button is clicked):
+
+<p align="center">
+<img src="http://shiny.rstudio.com/images/notification.png" alt="notification" width="50%"/>
+</p>
+
+[Here](http://shiny.rstudio.com/articles/notifications.html)'s our article about it, and the [reference documentation](http://shiny.rstudio.com/reference/shiny/latest/showNotification.html).
+
+## Progress indicators
+
+If your Shiny app contains computations that take a long time to complete, a progress bar can improve the user experience by communicating how far along the computation is, and how much is left. Progress bars were added in Shiny 0.10.2. In Shiny 0.14, they were changed to use the notifications system, which gives them a different look.
+
+**_Important note_:**
+> If you were already using progress bars and had customized them with your own CSS, you can add the `style = "old"` argument to your `withProgress()` call (or `Progress$new()`). This will result in the same appearance as before. You can also call `shinyOptions(progress.style = "old")` in your app's server function to make all progress indicators use the old styling.
+
+To see new progress bars in action, see [this app](https://gallery.shinyapps.io/085-progress/) in the gallery. You can also learn more about this in [our article](http://shiny.rstudio.com/articles/progress.html) and in the reference documentation (either for the easier [`withProgress` functional API](http://shiny.rstudio.com/reference/shiny/latest/withProgress.html) or the more complicated, but more powerful, [`Progress` object-oriented API](http://shiny.rstudio.com/reference/shiny/lates [...]
+
+## Reconnection
+
+Shiny can now automatically reconnect to your Shiny session if you temporarily lose network access.
+
+## Modal windows
+
+Shiny has now built-in support for displaying modal dialogs like the one below ([live app here](https://gallery.shinyapps.io/114-modal-dialog/)):
+
+<p align="center">
+<img src="http://shiny.rstudio.com/images/modal-dialog.png" alt="modal-dialog" width="50%"/>
+</p>
+
+To learn more about this, read [our article](http://shiny.rstudio.com/articles/modal-dialogs.html) and the [reference documentation](http://shiny.rstudio.com/reference/shiny/latest/modalDialog.html).
+
+## `insertUI` and `removeUI`
+
+Sometimes in a Shiny app, arbitrary HTML UI may need to be created on-the-fly in response to user input. The existing `uiOutput` and `renderUI` functions let you continue using reactive logic to call UI functions and make the results appear in a predetermined place in the UI. The `insertUI` and `removeUI` functions, which are used in the server code, allow you to use imperative logic to add and remove arbitrary chunks of HTML (all independent from one another), as many times as you want, [...]
+
+See [this simple demo app](https://gallery.shinyapps.io/111-insert-ui/) of how one could use `insertUI` and `removeUI` to insert and remove text elements using a queue. Also see [this other app](https://gallery.shinyapps.io/insertUI/) that demonstrates how to insert and remove a few common Shiny input objects. Finally, [this app](https://gallery.shinyapps.io/insertUI-modules/) shows how to dynamically insert modules using `insertUI`.
+
+For more, read [our article](http://shiny.rstudio.com/articles/dynamic-ui.html) about dynamic UI generation and the reference documentation about [`insertUI`](http://shiny.rstudio.com/reference/shiny/latest/insertUI.html) and [`removeUI`](http://shiny.rstudio.com/reference/shiny/latest/removeUI.html).
+
+## Documentation for connecting to an external database
+
+Many Shiny users have asked about best practices for accessing external databases from their Shiny applications. Although database access has long been possible using various database connector packages in R, it can be challenging to use them robustly in the dynamic environment that Shiny provides. So far, it has been mostly up to application authors to find the appropriate database drivers and to discover how to manage the database connections within an application. In order to demystif [...]
+
+There are a few packages that you should look at if you're using a relational database in a Shiny app: the `dplyr` and `DBI` packages (both featured in the article linked to above), and the brand new `pool` package, which provides a further layer of abstraction to make it easier and safer to use either `DBI` or `dplyr`. `pool` is not yet on CRAN. In particular, `pool` will take care of managing connections, preventing memory leaks, and ensuring the best performance. See this [`pool` basi [...]
+
+If you're new to databases in the Shiny world, we recommend using `dplyr` and `pool` if possible. If you need greater control than `dplyr` offers (for example, if you need to modify data in the database or use transactions), then use `DBI` and `pool`. The `pool` package was introduced to make your life easier, but in no way constrains you, so we don't envision any situation in which you'd be better off *not* using it. The only caveat is that `pool` is not yet on CRAN, so you may prefer t [...]
+
+## Others
+
+There are many more minor features, small improvements, and bug fixes than we can cover here, so we'll just mention a few of the more noteworthy ones (the full changelog, with links to all the relevant issues and pull requests, is right below this section):
+
+*    **Error Sanitization**: you now have the option to sanitize error messages; in other words, the content of the original error message can be suppressed so that it doesn't leak any sensitive information. To sanitize errors everywhere in your app, just add `options(shiny.sanitize.errors = TRUE)` somewhere in your app. Read [this article](http://shiny.rstudio.com/articles/sanitize-errors.html) for more, or play with the [demo app](https://gallery.shinyapps.io/110-error-sanitization/).
+
+*    **Code Diagnostics**: if there is an error parsing `ui.R`, `server.R`, `app.R`, or `global.R`, Shiny will search the code for missing commas, extra commas, and unmatched braces, parens, and brackets, and will print out messages pointing out those problems. ([#1126](https://github.com/rstudio/shiny/pull/1126))
+
+*    **Reactlog visualization**: by default, the [`showReactLog()` function](http://shiny.rstudio.com/reference/shiny/latest/showReactLog.html) (which brings up the reactive graph) also displays the time that each reactive and observer were active for:
+
+      <p align="center">
+      <img src="http://shiny.rstudio.com/images/reactlog.png" alt="modal-dialog" width="75%"/>
+      </p>
+
+      This new feature can be turned off with `showReactLog(time = FALSE)`. This may be convenient if you have a large graph and don't want to have this new information cluttering it up. The elapsed time info shows up above each relevant node's label, and the time is also coded by color: the slowest reactive will be dark red and the fastest will be light red.
+
+      Additionally, to organize the graph, you can now drag any of the nodes to a specific position and leave it there.
+
+*    **Nicer-looking tables**: we've made tables generated with `renderTable()` look cleaner and more modern. While this won't break any older code, the finished look of your table will be quite a bit different, as the following image shows:
+
+      <p align="center">
+      <img src="http://shiny.rstudio.com/images/render-table.png" alt="render-table" width="75%"/>
+      </p>
+
+      For more, read our [short article](http://shiny.rstudio.com/articles/render-table.html) about this update, experiment with all the new features in this [demo app](https://gallery.shinyapps.io/109-render-table/), or check out the [reference documentation](http://shiny.rstudio.com/reference/shiny/latest/renderTable.html).
+
+## Full changelog
+
+### Breaking changes
+
+* Progress indicators can now either use the new notification API, using `style = "notification"` (default), or be displayed with the previous styling, using `style = "old"`. You can also call `shinyOptions(progress.style = "old")` in the server function to make all progress indicators use the old styling. Note that if you had customized your progress indicators with additional CSS, you'll need to use the old style if you want your UI to look the same ([#1160](https://github.com/rstudio/ [...]
+
+* Closed [#1161](https://github.com/rstudio/shiny/issues/1161): Deprecated the `position` argument to `tabsetPanel()` since Bootstrap 3 stopped supporting this feature.
+
+* The long-deprecated ability to pass a `func` argument to many of the `render` functions has been removed.
+
+### New features
+
+* Added the ability to bookmark and restore application state. (main PR: [#1209](https://github.com/rstudio/shiny/pull/1209))
+
+* Added a new notification API. From R, there are new functions `showNotification` and `hideNotification`. From JavaScript, there is a new `Shiny.notification` object that controls notifications. ([#1141](https://github.com/rstudio/shiny/pull/1141))
+
+* Progress indicators now use the notification API. ([#1160](https://github.com/rstudio/shiny/pull/1160))
+
+* Added the ability for the client browser to reconnect to a new session on the server, by setting `session$allowReconnect(TRUE)`. This requires a version of Shiny Server that supports reconnections. ([#1074](https://github.com/rstudio/shiny/pull/1074))
+
+* Added modal dialogs. ([#1157](https://github.com/rstudio/shiny/pull/1157))
+
+* Added insertUI and removeUI functions to be able to add and remove chunks of UI, standalone, and all independent of one another. ([#1174](https://github.com/rstudio/shiny/pull/1174) and [#1189](https://github.com/rstudio/shiny/pull/1189))
+
+* Improved `renderTable()` function to make the tables look prettier and also provide the user with a lot more parameters to customize their tables with. ([#1129](https://github.com/rstudio/shiny/pull/1129))
+
+* Added support for the `pool` package (use Shiny's timer/scheduler). ([#1226](https://github.com/rstudio/shiny/pull/1226))
+
+### Minor new features and improvements
+
+* Added `cancelOutput` argument to `req()`. This causes the currently executing reactive to cancel its execution, and leave its previous state alone (as opposed to clearing the output). ([#1272](https://github.com/rstudio/shiny/pull/1272))
+
+* `Display: Showcase` now displays the .js, .html and .css files in the `www` directory by default. In order to use showcase mode and not display these, include a new line in your Description file: `IncludeWWW: False`. ([#1185](https://github.com/rstudio/shiny/pull/1185))
+
+* Added an error sanitization option: `options(shiny.sanitize.errors = TRUE)`. By default, this option is `FALSE`. When `TRUE`, normal errors will be sanitized, displaying only a generic error message. This changes the look of an app when errors are printed (but the console log remains the same). ([#1156](https://github.com/rstudio/shiny/pull/1156))
+
+* Added the option of passing arguments to an `xxxOutput()` function through the corresponding `renderXXX()` function via an `outputArgs` parameter to the latter. This is only valid for snippets of Shiny code in an interactive `runtime: shiny` Rmd document (never for full apps, even if embedded in an Rmd). ([#1443](https://github.com/rstudio/shiny/pull/1143))
+
+* Added `updateActionButton()` function, so the user can change an `actionButton`'s (or `actionLink`'s) label and/or icon. It also checks that the icon argument (for both creation and updating of a button) is valid and throws a warning otherwise. ([#1134](https://github.com/rstudio/shiny/pull/1134))
+
+* Added code diagnostics: if there is an error parsing ui.R, server.R, app.R, or global.R, Shiny will search the code for missing commas, extra commas, and unmatched braces, parens, and brackets, and will print out messages pointing out those problems. ([#1126](https://github.com/rstudio/shiny/pull/1126))
+
+* Added support for horizontal dividers in `navbarMenu`. ([#1147](https://github.com/rstudio/shiny/pull/1147))
+
+* Added `placeholder` option to `passwordInput`. ([#1152](https://github.com/rstudio/shiny/pull/1152))
+
+* Added `session$resetBrush(brushId)` (R) and `Shiny.resetBrush(brushId)` (JS) to programatically clear brushes from `imageOutput`/`plotOutput`. ([#1197](https://github.com/rstudio/shiny/pull/1197))
+
+* Added textAreaInput. (thanks, [@nuno-agostinho](https://github.com/nuno-agostinho)! [#1300](https://github.com/rstudio/shiny/pull/1300))
+
+* Added `session$sendBinaryMessage(type, message)` method for sending custom binary data to the client. See `?session`. (thanks, [@daef](https://github.com/daef)! [#1316](https://github.com/rstudio/shiny/pull/1316) and [#1320](https://github.com/rstudio/shiny/pull/1320))
+
+* Almost all code examples now have a runnable example with `shinyApp()`, so that users can run the examples and see them in action. ([#1158](https://github.com/rstudio/shiny/pull/1158))
+
+* When resized, plots are drawn with `replayPlot()`, instead of re-executing all plotting code. This results in faster plot rendering. ([#1112](https://github.com/rstudio/shiny/pull/1112))
+
+* Exported the `isTruthy()` function. (part of PR [#1272](https://github.com/rstudio/shiny/pull/1272))
+
+* Reactive log now shows elapsed time for reactives and observers. ([#1132](https://github.com/rstudio/shiny/pull/1132))
+
+* Nodes in the reactlog visualization are now sticky if the user drags them. ([#1283](https://github.com/rstudio/shiny/pull/1283))
+
+### Bug fixes
+
+* Fixed [#1350](https://github.com/rstudio/shiny/issues/1350): Highlighting of reactives didn't work in showcase mode.
+
+* Fixed [#1331](https://github.com/rstudio/shiny/issues/1331): `renderPlot()` now correctly records and replays plots when `execOnResize = FALSE`.
+
+* `updateDateInput()` and `updateDateRangeInput()` can now clear the date input fields. (thanks, [@gaborcsardi](https://github.com/gaborcsardi)! [#1299](https://github.com/rstudio/shiny/pull/1299), [#1315](https://github.com/rstudio/shiny/pull/1315) and  [#1317](https://github.com/rstudio/shiny/pull/1317))
+
+* Fixed [#561](https://github.com/rstudio/shiny/issues/561): DataTables previously might pop up a warning when the data was updated extremely frequently.
+
+* Fixed [#776](https://github.com/rstudio/shiny/issues/776): In some browsers, plots sometimes flickered when updated.
+
+* Fixed [#543](https://github.com/rstudio/shiny/issues/543) and [#855](https://github.com/rstudio/shiny/issues/855): When `navbarPage()` had a `navbarMenu()` as the first item, it did not automatically select an item.
+
+* Fixed [#970](https://github.com/rstudio/shiny/issues/970): `navbarPage()` previously did not have an option to set the selected tab.
+
+* Fixed [#1253](https://github.com/rstudio/shiny/issues/1253): Memory could leak when an observer was destroyed without first being invalidated.
+
+* Fixed [#931](https://github.com/rstudio/shiny/issues/931): Nested observers could leak memory.
+
+* Fixed [#1144](https://github.com/rstudio/shiny/issues/1144): `updateRadioButton()` and `updateCheckboxGroupInput()` broke controls when used in modules (thanks, [@sipemu](https://github.com/sipemu)!).
+
+* Fixed [#1093](https://github.com/rstudio/shiny/issues/1093): `updateRadioButtons()` and `updateCheckboxGroupInput()` didn't work if `choices` was numeric vector.
+
+* Fixed [#1122](https://github.com/rstudio/shiny/issues/1122): `downloadHandler()` popped up empty browser window if the file wasn't present. It now gives a 404 error code.
+
+* Fixed [#1278](https://github.com/rstudio/shiny/issues/1278): Reactive system was being flushed too often (usually this just means a more-expensive no-op than necessary).
+
+* Fixed [#803](https://github.com/rstudio/shiny/issues/803) and [#1179](https://github.com/rstudio/shiny/issues/1179): handling malformed dates in `dateInput` and `updateDateInput()`.
+
+* Fixed [#1257](https://github.com/rstudio/shiny/issues/1257): `updateSelectInput()` didn't work correctly in IE 11 and Edge.
+
+* Fixed [#971](https://github.com/rstudio/shiny/issues/971): `runApp()` would give confusing error if `port` was not numeric.
+
+* Shiny now avoids using ports that Chrome deems unsafe. ([#1222](https://github.com/rstudio/shiny/pull/1222))
+
+* Added workaround for quartz graphics device resolution bug, where resolution is hard-coded to 72 ppi.
+
+### Library updates
+
+* Updated to ion.RangeSlider 2.1.2.
+
+* Updated to Font Awesome 4.6.3.
+
+* Updated to Bootstrap 3.3.7.
+
+* Updated to jQuery 1.12.4.
+
+
+shiny 0.13.2
+============
+
+* Updated documentation for `htmlTemplate`.
+
+
+shiny 0.13.1
+============
+
+* `flexCol` did not work on RStudio for Windows or Linux.
+
+* Fixed RStudio debugger integration.
+
+* BREAKING CHANGE: The long-deprecated ability to pass functions (rather than expressions) to reactive() and observe() has finally been removed.
+
+
+shiny 0.13.0
+============
+
+* Fixed #962: plot interactions did not work with the development version of ggplot2 (after ggplot2 1.0.1).
+
+* Fixed #902: the `drag_drop` plugin of the selectize input did not work.
+
+* Fixed #933: `updateSliderInput()` does not work when only the label is updated.
+
+* Multiple imageOutput/plotOutput calls can now share the same brush id. Shiny will ensure that performing a brush operation will clear any other brush with the same id.
+
+* Added `placeholder` option to `textInput`.
+
+* Improved support for Unicode characters on Windows (#968).
+
+* Fixed bug in `selectInput` and `selectizeInput` where values with double quotes were not properly escaped.
+
+* `runApp()` can now take a path to any .R file that yields a `shinyApp` object; previously, the path had to be a directory that contained an app.R file (or server.R if using separately defined server and UI). Similarly, introduced `shinyAppFile()` function which creates a `shinyApp` object for an .R file path, just as `shinyAppDir()` does for a directory path.
+
+* Introduced Shiny Modules, which are designed to 1) simplify the reuse of Shiny UI/server logic and 2) make authoring and maintaining complex Shiny apps much easier. See the article linked from `?callModule`.
+
+* `invalidateLater` and `reactiveTimer` no longer require an explicit `session` argument; the default value uses the current session.
+
+* Added `session$reload()` method, the equivalent of hitting the browser's Reload button.
+
+* Added `shiny.autoreload` option, which will automatically cause browsers to reload whenever Shiny app files change on disk. This is intended to shorten the feedback cycle when tweaking UI code.
+
+* Errors are now printed with stack traces! This should make it tremendously easier to track down the causes of errors in Shiny. Try it by calling `stop("message")` from within an output, reactive, or observer. Shiny itself adds a lot of noise to the call stack, so by default, it attempts to hide all but the relevant levels of the call stack. You can turn off this behavior by setting `options(shiny.fullstacktrace=TRUE)` before or during app startup.
+
+* Fixed #1018: the selected value of a selectize input is guaranteed to be selected in server-side mode.
+
+* Added `req` function, which provides a simple way to prevent a reactive, observer, or output from executing until all required inputs and values are available. (Similar functionality has been available for a while using validate/need, but req provides a much simpler and more direct interface.)
+
+* Improve stability with Shiny Server when many subapps are used, by deferring the loading of subapp iframes until a connection has first been established with the server.
+
+* Upgrade to Font Awesome 4.5.0.
+
+* Upgraded to Bootstrap 3.3.5.
+
+* Upgraded to jQuery 1.12.4
+
+* Switched to an almost-complete build of jQuery UI with the exception of the datepicker extension, which conflicts with Shiny's date picker.
+
+* Added `fillPage` function, an alternative to `fluidPage`, `fixedPage`, etc. that is designed for apps that fill the entire available page width/height.
+
+* Added `fillRow` and `fillCol` functions, for laying out proportional grids in `fillPage`. For modern browsers only.
+
+* Added `runGadget`, `paneViewer`, `dialogViewer`, and `browserViewer` functions to support Shiny Gadgets. More detailed docs about gadgets coming soon.
+
+* Added support for the new htmltools 0.3 feature `htmlTemplate`. It's now possible to use regular HTML markup to design your UI, but still use R expressions to define inputs, outputs, and HTML widgets.
+
+
+shiny 0.12.2
+============
+
+* GitHub changed URLs for gists from .tar.gz to .zip, so `runGist` was updated to work with the new URLs.
+
+* Callbacks from the session object are now guaranteed to execute in the order in which registration occurred.
+
+* Minor bugs in sliderInput's animation behavior have been fixed. (#852)
+
+* Updated to ion.rangeSlider to 2.0.12.
+
+* Added `shiny.minified` option, which controls whether the minified version of shiny.js is used. Setting it to FALSe can be useful for debugging. (#826, #850)
+
+* Fixed an issue for outputting plots from ggplot objects which also have an additional class whose print method takes precedence over `print.ggplot`. (#840, 841)
+
+* Added `width` option to Shiny's input functions. (#589, #834)
+
+* Added two alias functions of `updateTabsetPanel()` to update the selected tab: `updateNavbarPage()` and `updateNavlistPanel()`. (#881)
+
+* All non-base functions are now explicitly namespaced, to pass checks in R-devel.
+
+* Shiny now correctly handles HTTP HEAD requests. (#876)
+
+
+shiny 0.12.1
+============
+
+* Fixed an issue where unbindAll() causes subsequent bindAll() to be ignored for previously bound outputs. (#856)
+
+* Undeprecate `dataTableOutput` and `renderDataTable`, which had been deprecated in favor of the new DT package. The DT package is a bit too new and has a slightly different API, we were too hasty in deprecating the existing Shiny functions.
+
+
+shiny 0.12.0
+============
+
+In addition to the changes listed below (in the *Full Changelog* section), there is an infrastructure change that could affect existing Shiny apps. 
+
+### JSON serialization
+
+In Shiny 0.12.0, we've switched from RJSONIO to jsonlite. For the vast majority of users, this will result in no noticeable changes; however, if you use any packages in your Shiny apps which rely on the [htmlwidgets](http://www.htmlwidgets.org/), you will also need to update htmlwidgets to 0.4.0. Both of these packages will issue a message when loaded, if the other package needs to be upgraded.
+
+POSIXt objects are now serialized to JSON in UTC8601 format (like
+"2015-03-20T20:00:00Z"), instead of as seconds from the epoch. If you have a Shiny app which uses `sendCustomMessage()` to send datetime (POSIXt) objects, then you may need to modify your Javascript code to receive time data in this format.
+
+### A note about Data Tables
+
+Shiny 0.12.0 deprecated Shiny's dataTableOutput and renderDataTable functions and instructed you to migrate to the nascent [DT](https://rstudio.github.io/DT/) package instead. (We'll talk more about DT in a future blog post.) User feedback has indicated this transition was too sudden and abrupt, so we've undeprecated these functions in 0.12.1. We'll continue to support these functions until DT has had more time to mature.
+
+## Full Changelog
+
+* Switched from RJSONIO to jsonlite. This improves consistency and speed when converting between R data structures and JSON. One notable change is that POSIXt objects are now serialized to JSON in UTC8601 format (like "2015-03-20T20:00:00Z"), instead of as seconds from the epoch).
+
+* In addition to the existing support for clicking and hovering on plots created by base graphics, added support for double-clicking and brushing. (#769)
+
+* Added support for clicking, hovering, double-clicking, and brushing for plots created by ggplot2, including support for facets. (#802)
+
+* Added `nearPoints` and `brushedPoints` functions for easily selecting rows of data that are clicked/hovered, or brushed. (#802)
+
+* Added `shiny.port` option. If this is option is set, `runApp()` will listen on this port by default. (#756)
+
+* `runUrl`, `runGist`, and `runGitHub` now can save downloaded applications, with the `destdir` argument. (#688)
+
+* Restored ability to set labels for `selectInput`. (#741)
+
+* Travis continuous integration now uses Travis's native R support.
+
+* Fixed encoding issue when the server receives data from the client browser. (#742)
+
+* The `session` object now has class `ShinySession`, making it easier to test whether an object is indeed a session object. (#720, #746)
+
+* Fix JavaScript error when an output appears in nested uiOutputs. (Thanks, Gregory Zhang. #749)
+
+* Eliminate delay on receiving new value when `updateSliderInput(value=...)` is called.
+
+* Updated to DataTables (Javascript library) 1.10.5.
+
+* Fixed downloading of files that have no filename extension. (#575, #753)
+
+* Fixed bug where nested UI outputs broke outputs. (#749, #750)
+
+* Removed unneeded HTML ID attributes for `checkboxGroupInputs` and `radioButtons`. (#684)
+
+* Fixed bug where checkboxes were still active even after `Shiny.unbindAll()` was called. (#206)
+
+* The server side selectize input will load the first 1000 options by default before users start to type and search in the box. (#823)
+
+* renderDataTable() and dataTableOutput() have been deprecated in shiny and will be removed in future versions of shiny. Please use the DT package instead: http://rstudio.github.io/DT/ (#807)
+
+
+shiny 0.11.1
+============
+
+* Major client-side performance improvements for pages that have many conditionalPanels, tabPanels, and plotOutputs. (#693, #717, #723)
+
+* `tabPanel`s now use the `title` for `value` by default. This fixes a bug in which an icon in the title caused problems with a conditionalPanel's test condition. (#725, #728)
+
+* `selectInput` now has a `size` argument to control the height of the input box. (#729)
+
+* `navbarPage` no longer includes a first row of extra whitespace when `header=NULL`. (#722)
+
+* `selectInput`s now use Bootstrap styling when `selectize=FALSE`. (#724)
+
+* Better vertical spacing of label for checkbox groups and radio buttons.
+
+* `selectInput` correctly uses width for both selectize and non-selectize inputs. (#702)
+
+* The wrapper tag generated by `htmlOutput` and `uiOutput` can now be any type of HTML tag, instead of just span and div. Also, custom classes are now allowed on the tag. (#704)
+
+* Slider problems in IE 11 and Chrome on touchscreen-equipped Windows computers have been fixed. (#700)
+
+* Sliders now work correctly with draggable panels. (#711)
+
+* Fixed arguments in `fixedPanel`. (#709)
+
+* downloadHandler content callback functions are now invoked with a temp file name that has the same extension as the final filename that will be used by the download. This is to deal with the fact that some file writing functions in R will auto-append the extension for their file type (pdf, zip).
+
+
+shiny 0.11
+==========
+
+Shiny 0.11 switches away from the Bootstrap 2 web framework to the next version, Bootstrap 3. This is in part because Bootstrap 2 is no longer being developed, and in part because it allows us to tap into the ecosystem of Bootstrap 3 themes.
+
+
+### Known issues for migration
+
+*    In Bootstrap 3, images in `<img>` tags are no longer automatically scaled to the width of their container. If you use `img()` in your UI code, or `<img>` tags in your raw HTML source, it's possible that they will be too large in the new version of Shiny. To address this you can add the `img-responsive` class:
+    
+    ```r
+    img(src = "picture.png", class = "img-responsive")
+    ```
+    
+    The R code above will generate the following HTML:
+    
+    ```html
+    <img src="picture.png" class="img-responsive">
+    ```
+
+
+*    The sliders have been replaced. Previously, Shiny used the [jslider](https://github.com/egorkhmelev/jslider) library, but now it uses [ion.RangeSlider](https://github.com/IonDen/ion.rangeSlider). The new sliders have an updated appearance, and they have allowed us to fix many long-standing interface issues with the sliders.
+
+  * The `sliderInput()` function no longer uses the `format` or `locale` options. Instead, you can use `pre`, `post`, and `sep` options to control the prefix, postfix, and thousands separator.
+
+
+  * `updateSliderInput()` can now control the min, max, value, and step size of a slider. Previously, only the value could be controlled this way, and if you wanted to change other values, you needed to use Shiny's dynamic UI.
+
+
+*    If in your HTML you are using custom CSS classes that are specific to Bootstrap, you may need to update them for Bootstrap 3. See the Bootstrap [migration guide](http://getbootstrap.com/migration/).
+
+
+If you encounter other migration issues, please let us know on the [shiny-discuss](https://groups.google.com/forum/#!forum/shiny-discuss) mailing list, or on the Shiny [issue tracker](https://github.com/rstudio/shiny/issues).
+
+### Using shinybootstrap2
+
+If you would like to use Shiny 0.11 with Bootstrap 2, you can use the **shinybootstrap2** package. Installation and usage instructions are on available on the [project page](https://github.com/rstudio/shinybootstrap2). We recommend that you do this only as a temporary solution because  future development on Shiny will use Bootstrap 3.
+
+### Installing an older version of Shiny
+
+If you want to install a specific version of Shiny other than the latest CRAN release, you can use the `install_version()` function from devtools:
+
+```r
+# Install devtools if you don't already have it:
+install.package("devtools")
+
+# Install the last version of Shiny prior to 0.11
+devtools::install_version("shiny", "0.10.2.2")
+```
+
+### Themes
+
+Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes in the [shinythemes](http://rstudio.github.io/shinythemes/) package. This package makes it easy to use Bootstrap themes with Shiny.
+
+## Full Changelog
+
+* Changed sliders from jquery-slider to ion.rangeSlider. These sliders have an improved appearance, support updating more properties from the server, and can be controlled with keyboard input.
+
+* Switched from Bootstrap 2 to Bootstrap 3. For most users, this will work seamlessly, but some users may need to use the shinybootstrap2 package for backward compatibility.
+
+* The UI of a Shiny app can now have a body tag. This is useful for CSS templates that use classes on the body tag.
+
+* `actionButton` and `actionLink` now pass their `...` arguments to the underlying tag function. (#607)
+
+* Added `observeEvent` and `eventReactive` functions for clearer, more concise handling of `actionButton`, plot clicks, and other naturally-imperative inputs.
+
+* Errors that happen in reactives no longer prevent any remaining pending observers from executing. It is also now possible for users to control how errors are handled, with the 'shiny.observer.error' global option. (#603, #604)
+
+* Added an `escape` argument to `renderDataTable()` to escape the HTML entities in the data table for security reasons. This might break tables from previous versions of shiny that use raw HTML in the table content, and the old behavior can be brought back by `escape = FALSE` if you are aware of the security implications. (#627)
+
+* Changed the URI encoding/decoding functions internally to use `encodeURI()`, `encodeURIComponent()`, and `decodeURIComponent()` from the httpuv package instead of `utils::URLencode()` and `utils::URLdecode()`. (#630)
+
+* Shiny's web assets are now minified.
+
+* The default reactive domain is now available in event handler functions. (#669)
+
+* Password input fields can now be used, with `passwordInput()`. (#672)
+
+
+shiny 0.10.2.2
+==============
+
+* Remove use of `rstudio::viewer` in a code example, for R CMD check.
+
+
+shiny 0.10.2.1
+==============
+
+* Changed some examples to use \donttest instead of \dontrun.
+
+
+shiny 0.10.2
+============
+
+* The minimal version of R required for the shiny package is 3.0.0 now.
+
+* Shiny apps can now consist of a single file, app.R, instead of ui.R and server.R.
+
+* Upgraded DataTables from 1.9.4 to 1.10.2. This might be a breaking change if you have customized the DataTables options in your apps. (More info: https://github.com/rstudio/shiny/pull/558)
+
+* File uploading via `fileInput()` works for Internet Explorer 8 and 9 now. Note: IE8/9 do not support multiple files from a single file input. If you need to upload multiple files, you have to use one file input for each file.
+
+* Switched away from reference classes to R6.
+
+* Reactive log performance has been greatly improved.
+
+* Added `Progress` and `withProgress`, to display the progress of computation on the client browser.
+
+* Fixed #557: updateSelectizeInput(choices, server = TRUE) did not work when `choices` is a character vector.
+
+* Searching in DataTables is case-insensitive and the search strings are not treated as regular expressions by default now. If you want case-sensitive searching or regular expressions, you can use the configuration options `search$caseInsensitive` and `search$regex`, e.g. `renderDataTable(..., options = list(search = list(caseInsensitve = FALSE, regex = TRUE)))`.
+
+* Added support for `htmltools::htmlDependency`'s new `attachment` parameter to `renderUI`/`uiOutput`.
+
+* Exported `createWebDependency`. It takes an `htmltools::htmlDependency` object and makes it available over Shiny's built-in web server.
+
+* Custom output bindings can now render `htmltools::htmlDependency` objects at runtime using `Shiny.renderDependencies()`.
+
+* Fixes to rounding behavior of sliderInput. (#301, #502)
+
+* Updated selectize.js to version 0.11.2. (#596)
+
+* Added `position` parameter to `navbarPage`.
+
+
+shiny 0.10.1
+============
+
+* Added Unicode support for Windows. Shiny apps running on Windows must use the UTF-8 encoding for ui.R and server.R (also the optional global.R) if they contain non-ASCII characters. See this article for details and examples: http://shiny.rstudio.com/gallery/unicode-characters.html (#516)
+
+* `runGitHub()` also allows the 'username/repo' syntax now, which is equivalent to `runGitHub('repo', 'username')`. (#427)
+
+* `navbarPage()` now accepts a `windowTitle` parameter to set the web browser page title to something other than the title displayed in the navbar.
+
+* Added an `inline` argument to `textOutput()`, `imageOutput()`, `plotOutput()`, and `htmlOutput()`. When `inline = TRUE`, these outputs will be put in `span()` instead of the default `div()`. This occurs automatically when these outputs are created via the inline expressions (e.g. `r renderText(expr)`) in R Markdown documents. See an R Markdown example at http://shiny.rstudio.com/gallery/inline-output.html (#512)
+
+* Added support for option groups in the select/selectize inputs. When the `choices` argument for `selectInput()`/`selectizeInput()` is a list of sub-lists and any sub-list is of length greater than 1, the HTML tag `<optgroup>` will be used. See an example at http://shiny.rstudio.com/gallery/option-groups-for-selectize-input.html (#542)
+
+
+shiny 0.10.0
+============
+
+* BREAKING CHANGE: By default, observers now terminate themselves if they were created during a session and that session ends. See ?domains for more details.
+
+* Shiny can now be used in R Markdown v2 documents, to create "Shiny Docs": reports and presentations that combine narrative, statically computed output, and fully dynamic inputs and outputs. For more info, including examples, see http://rmarkdown.rstudio.com/authoring_shiny.html.
+
+* The `session` object that can be passed into a server function (e.g. shinyServer(function(input, output, session) {...})) is now documented: see `?session`.
+
+* Most inputs can now accept `NULL` label values to omit the label altogether.
+
+* New `actionLink` input control; like `actionButton`, but with the appearance of a normal link.
+
+* `renderPlot` now calls `print` on its result if it's visible (i.e. no more explicit `print()` required for ggplot2).
+
+* Introduced Shiny app objects (see `?shinyApp`). These essentially replace the little-advertised ability for `runApp` to take a `list(ui=..., server=...)` as the first argument instead of a directory (though that ability remains for backward compatibility). Unlike those lists, Shiny app objects are tagged with class `shiny.appobj` so they can be run simply by printing them.
+
+* Added `maskReactiveContext` function. It blocks the current reactive context, to evaluate expressions that shouldn't use reactive sources directly. (This should not be commonly needed.)
+
+* Added `flowLayout`, `splitLayout`, and `inputPanel` functions for putting UI elements side by side. `flowLayout` lays out its children in a left-to-right, top-to-bottom arrangement. `splitLayout` evenly divides its horizontal space among its children (or unevenly divides if `cellWidths` argument is provided). `inputPanel` is like `flowPanel`, but with a light grey background, and is intended to be used to encapsulate small input controls wherever vertical space is at a premium.
+
+* Added `serverInfo` to obtain info about the Shiny Server if the app is served through it.
+
+* Added an `inline` argument (TRUE/FALSE) in `checkboxGroupInput()` and `radioButtons()` to allow the horizontal layout (inline = TRUE) of checkboxes or radio buttons. (Thanks, @saurfang, #481)
+
+* `sliderInput` and `selectizeInput`/`selectInput` now use a standard horizontal size instead of filling up all available horizontal space. Pass `width="100%"` explicitly for the old behavior.
+
+* Added the `updateSelectizeInput()` function to make it possible to process searching on the server side (i.e. using R), which can be much faster than the client side processing (i.e. using HTML and JavaScript). See the article at http://shiny.rstudio.com/articles/selectize.html for a detailed introduction.
+
+* Fixed a bug of renderDataTable() when the data object only has 1 row and 1 column. (Thanks, ZJ Dai, #429)
+
+* `renderPrint` gained a new argument 'width' to control the width of the text output, e.g. renderPrint({mtcars}, width = 40).
+
+* Fixed #220: the zip file for a directory created by some programs may not have the directory name as its first entry, in which case runUrl() can fail. (#220)
+
+* `runGitHub()` can also take a value of the form "username/repo" in its first argument, e.g. both runGitHub("shiny_example", "rstudio") and runGitHub("rstudio/shiny_example") are valid ways to run the GitHub repo.
+
+
+shiny 0.9.1
+===========
+
+* Fixed warning 'Error in Context$new : could not find function "loadMethod"' that was happening to dependent packages on "R CMD check".
+
+
+shiny 0.9.0
+===========
+
+* BREAKING CHANGE: Added a `host` parameter to runApp() and runExample(), which defaults to the shiny.host option if it is non-NULL, or "127.0.0.1" otherwise. This means that by default, Shiny applications can only be accessed on the same machine from which they are served. To allow other clients to connect, as in previous versions of Shiny, use "0.0.0.0" (or the IP address of one of your network interfaces, if you care to be explicit about it).
+
+* Added a new function `selectizeInput()` to use the JavaScript library selectize.js (https://github.com/brianreavis/selectize.js), which extends the basic select input in many aspects.
+
+* The `selectInput()` function also gained a new argument `selectize = TRUE` to makes use of selectize.js by default. If you want to revert back to the original select input, you have to call selectInput(..., selectize = FALSE).
+
+* Added Showcase mode, which displays the R code for an app right in the app itself. You can invoke Showcase mode by passing `display.mode="showcase"` to the `runApp()` function. Or, if an app is designed to run in Showcase mode by default, add a DESCRIPTION file in the app dir with Title, Author, and License fields; with "Type: Shiny"; and with "DisplayMode: Showcase".
+
+* Upgraded to Bootstrap 2.3.2 and jQuery 1.11.0.
+
+* Make `tags$head()` and `singleton()` behave correctly when used with `renderUI()` and `uiOutput()`. Previously, "hoisting content to the head" and "only rendering items a single time" were features that worked only when the page was initially loading, not in dynamic rendering.
+
+* Files are now sourced with the `keep.source` option, to help with debugging and profiling.
+
+* Support user-defined input parsers for data coming in from JavaScript using the parseShinyInput method.
+
+* Fixed the bug #299: renderDataTable() can deal with 0-row data frames now. (reported by Harlan Harris)
+
+* Added `navbarPage()` and `navbarMenu()` functions to create applications with multiple top level panels.
+
+* Added `navlistPanel()` function to create layouts with a bootstrap navlist on the left and tabPanels on the right
+
+* Added `type` parameter to `tabsetPanel()` to enable the use of pill style tabs in addition to the standard ones.
+
+* Added `position` paramter to `tabsetPanel()` to enable positioning of tabs above, below, left, or right of tab content.
+
+* Added `fluidPage()` and `fixedPage()` functions as well as related row and column layout functions for creating arbitrary bootstrap grid layouts.
+
+* Added `hr()` builder function for creating horizontal rules.
+
+* Automatically concatenate duplicate attributes in tag definitions
+
+* Added `responsive` parameter to page building functions for opting-out of bootstrap responsive css.
+
+* Added `theme` parameter to page building functions for specifying alternate bootstrap css styles.
+
+* Added `icon()` function for embedding icons from the [font awesome](http://fontawesome.io/) icon library
+
+* Added `makeReactiveBinding` function to turn a "regular" variable into a reactive one (i.e. reading the variable makes the current reactive context dependent on it, and setting the variable is a source of reactivity).
+
+* Added a function `withMathJax()` to include the MathJax library in an app.
+
+* The argument `selected` in checkboxGroupInput(), selectInput(), and radioButtons() refers to the value(s) instead of the name(s) of the argument `choices` now. For example, the value of the `selected` argument in selectInput(..., choices = c('Label 1' = 'x1', 'Label 2' = 'x2'), selected = 'Label 2') must be updated to 'x2', although names/labels will be automatically converted to values internally for backward compatibility. The same change applies to updateCheckboxGroupInput(), update [...]
+
+* Now it is possible to only update the value of a checkbox group, select input, or radio buttons using the `selected` argument without providing the `choices` argument in updateCheckboxGroupInput(), updateSelectInput(), and updateRadioButtons(), respectively. (#340)
+
+* Added `absolutePanel` and `fixedPanel` functions for creating absolute- and fixed-position panels. They can be easily made user-draggable by specifying `draggable = TRUE`.
+
+* For the `options` argument of the function `renderDataTable()`, we can pass literal JavaScript code to the DataTables library via `I()`. This makes it possible to use any JavaScript object in the options, e.g. a JavaScript function (which is not supported in JSON). See `?renderDataTable` for details and examples.
+
+* DataTables also works under IE8 now.
+
+* Fixed a bug in DataTables pagination when searching is turned on, which caused failures for matrices as well as empty rows when displaying data frames using renderDataTable().
+
+* The `options` argument in `renderDataTable()` can also take a function that returns a list. This makes it possible to use reactive values in the options. (#392)
+
+* `renderDataTable()` respects more DataTables options now: (1) either bPaginate = FALSE or iDisplayLength = -1 will disable pagination (i.e. all rows are returned from the data); besides, this means we can also use -1 in the length menu, e.g. aLengthMenu = list(c(10, 30, -1), list(10, 30, 'All')); (2) we can disable searching for individual columns through the bSearchable option, e.g. aoColumns = list(list(bSearchable = FALSE), list(bSearchable = TRUE),...) (the search box for the first [...]
+
+* Added an argument `callback` in `renderDataTable()` so that a custom JavaScript function can be applied to the DataTable object. This makes it much easier to use DataTables plug-ins.
+
+* For numeric columns in a DataTable, the search boxes support lower and upper bounds now: a search query of the form "lower,upper" (without quotes) indicates the limits [lower, upper]. For a column X, this means the rows corresponding to X >= lower & X <= upper are returned. If we omit either the lower limit or the upper limit, only the other limit will be used, e.g. ",upper" means X <= upper.
+
+* `updateNumericInput(value)` tries to preserve numeric precision by avoiding scientific notation when possible, e.g. 102145 is no longer rounded to 1.0214e+05 = 102140. (Thanks, Martin Loos. #401)
+
+* `sliderInput()` no longer treats a label wrapped in HTML() as plain text, e.g. the label in sliderInput(..., label = HTML('<em>A Label</em>')) will not be escaped any more. (#119)
+
+* Fixed #306: the trailing slash in a path could fail `addResourcePath()` under Windows. (Thanks, ZJ Dai)
+
+* Dots are now legal characters for inputId/outputId. (Thanks, Kevin Lindquist. #358)
+
+
+shiny 0.8.0
+===========
+
+* Debug hooks are registered on all user-provided functions and (reactive) expressions (e.g., in renderPlot()), which makes it possible to set breakpoints in these functions using the latest version of the RStudio IDE, and the RStudio visual debugging tools can be used to debug Shiny apps. Internally, the registration is done via installExprFunction(), which is a new function introduced in this version to replace exprToFunction() so that the registration can be automatically done.
+
+* Added a new function renderDataTable() to display tables using the JavaScript library DataTables. It includes basic features like pagination, searching (global search or search by individual columns), sorting (by single or multiple columns). All these features are implemented on the R side; for example, we can use R regular expressions for searching. Besides, it also uses the Bootstrap CSS style. See the full documentation and examples in the tutorial: http://rstudio.github.io/shiny/tu [...]
+
+* Added a new option `shiny.error` which can take a function as an error handler. It is called when an error occurs in an app (in user-provided code), e.g., after we set options(shiny.error = recover), we can enter a specified environment in the call stack to debug our code after an error occurs.
+
+* The argument `launch.browser` in runApp() can also be a function, which takes the URL of the shiny app as its input value.
+
+* runApp() uses a random port between 3000 and 8000 instead of 8100 now. It will try up to 20 ports in case certain ports are not available.
+
+* Fixed a bug for conditional panels: the value `input.id` in the condition was not correctly retrieved when the input widget had a type, such as numericInput(). (reported by Jason Bryer)
+
+* Fixed two bugs in plotOutput(); clickId and hoverId did not give correct coordinates in Firefox, or when the axis limits of the plot were changed. (reported by Chris Warth and Greg D)
+
+* The minimal required version for the httpuv package was increased to 1.2 (on CRAN now).
+
+
+shiny 0.7.0
+===========
+
+* Stopped sending websocket subprotocol. This fixes a compatibility issue with Google Chrome 30.
+
+* The `input` and `output` objects are now also accessible via `session$input` and `session$output`.
+
+* Added click and hover events for static plots; see `?plotOutput` for details.
+
+* Added optional logging of the execution states of a reactive program, and tools for visualizing the log data. To use, start a new R session and call `options(shiny.reactlog=TRUE)`. Then launch a Shiny app and interact with it. Press Ctrl+F3 (or for Mac, Cmd+F3) in the browser to launch an interactive visualization of the reactivity that has occurred. See `?showReactLog` for more information.
+
+* Added `includeScript()` and `includeCSS()` functions.
+
+* Reactive expressions now have class="reactive" attribute. Also added `is.reactive()` and `is.reactivevalues()` functions.
+
+* New `stopApp()` function, which stops an app and returns a value to the caller of `runApp()`.
+
+* Added the `shiny.usecairo` option, which can be used to tell Shiny not to use Cairo for PNG output even when it is installed. (Defaults to `TRUE`.)
+
+* Speed increases for `selectInput()` and `radioButtons()`, and their corresponding updater functions, for when they have many options.
+
+* Added `tagSetChildren()` and `tagAppendChildren()` functions.
+
+* The HTTP request object that created the websocket is now accessible from the `session` object, as `session$request`. This is a Rook-like request environment that can be used to access HTTP headers, among other things. (Note: When running in a Shiny Server environment, the request will reflect the proxy HTTP request that was made from the Shiny Server process to the R process, not the request that was made from the web browser to Shiny Server.)
+
+* Fix `getComputedStyle` issue, for IE8 browser compatibility (#196). Note: Shiny Server is still required for IE8/9 compatibility.
+
+* Add shiny.sharedSecret option, to require the HTTP header Shiny-Shared-Secret to be set to the given value.
+
+
+shiny 0.6.0
+===========
+
+* `tabsetPanel()` can be directed to start with a specific tab selected.
+
+* Fix bug where multiple file uploads with 3 or more files result in incorrect data.
+
+* Add `withTags()` function.
+
+* Add dateInput and dateRangeInput.
+
+* `shinyServer()` now takes an optional `session` argument, which is used for communication with the session object.
+
+* Add functions to update values of existing inputs on a page, instead of replacing them entirely.
+
+* Allow listening on domain sockets.
+
+* Added `actionButton()` to Shiny.
+
+* The server can now send custom JSON messages to the client. On the client side, functions can be registered to handle these messages.
+
+* Callbacks can be registered to be called at the end of a client session.
+
+* Add ability to set priority of observers and outputs. Each priority level gets its own queue.
+
+* Fix bug where the presence of a submit button would prevent sending of metadata until the button was clicked.
+
+* `reactiveTimer()` and `invalidateLater()` by default no longer invalidate reactive objects after the client session has closed.
+
+* Shiny apps can be run without a server.r and ui.r file.
+
+
+shiny 0.5.0
+===========
+
+* Switch from websockets package for handling websocket connections to httpuv.
+
+* New method for detecting hidden output objects. Instead of checking that height and width are 0, it checks that the object or any ancestor in the DOM has style display:none.
+
+* Add `clientData` reactive values object, which carries information about the client. This includes the hidden status of output objects, height/width plot output objects, and the URL of the browser.
+
+* Add `parseQueryString()` function.
+
+* Add `renderImage()` function for sending arbitrary image files to the client, and its counterpart, `imageOutput()`.
+
+* Add support for high-resolution (Retina) displays.
+
+* Fix bug #55, where `renderTable()` would throw error with an empty data frame.
+
+
+shiny 0.4.1
+===========
+
+* Fix bug where width and height weren't passed along properly from `reactivePlot` to `renderPlot`.
+
+* Fix bug where infinite recursion would happen when `reactivePlot` was passed a function for width or height.
+
+
+shiny 0.4.0
+===========
+
+* Added suspend/resume capability to observers.
+
+* Output objects are automatically suspended when they are hidden on the user's web browser.
+
+* `runGist()` accepts GitHub's new URL format, which includes the username.
+
+* `reactive()` and `observe()` now take expressions instead of functions.
+
+* `reactiveText()`, `reactivePlot()`, and so on, have been renamed to `renderText()`, `renderPlot()`, etc.  They also now take expressions instead of functions.
+
+* Fixed a bug where empty values in a numericInput were sent to the R process as 0. They are now sent as NA.
+
+
+shiny 0.3.1
+===========
+
+* Fix issue #91: bug where downloading files did not work.
+
+* Add [[<- operator for shinyoutput object, making it possible to assign values with `output[['plot1']] <- ...`.
+
+* Reactive functions now preserve the visible/invisible state of their returned values.
+
+
+shiny 0.3.0
+===========
+
+* Reactive functions are now evaluated lazily.
+
+* Add `reactiveValues()`.
+
+* Using `as.list()` to convert a reactivevalues object (like `input`) to a list is deprecated. The new function `reactiveValuesToList()` should be used instead.
+
+* Add `isolate()`. This function is used for accessing reactive functions, without them invalidating their parent contexts.
+
+* Fix issue #58: bug where reactive functions are not re-run when all items in a checkboxGroup are unchecked.
+
+* Fix issue #71, where `reactiveTable()` would return blank if the first element of a data frame was NA.
+
+* In `plotOutput`, better validation for CSS units when specifying width and height.
+
+* `reactivePrint()` no longer displays invisible output.
+
+* `reactiveText()` no longer displays printed output, only the return value from a function.
+
+* The `runGitHub()` and `runUrl()` functions have been added, for running Shiny apps from GitHub repositories and zip/tar files at remote URLs.
+
+* Fix issue #64, where pressing Enter in a textbox would cause a form to submit.
+
+
+shiny 0.2.4
+===========
+
+* `runGist` has been updated to use the new download URLs from https://gist.github.com.
+
+* Shiny now uses `CairoPNG()` for output, when the Cairo package is available. This provides better-looking output on Linux and Windows.
+
+
+shiny 0.2.3
+===========
+
+* Ignore request variables for routing purposes
+
+
+shiny 0.2.2
+===========
+
+* Fix CRAN warning (assigning to global environment)
+
+
+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 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
+
+
+shiny 0.1.2
+===========
+
+* Initial private beta release!
diff --git a/R/app.R b/R/app.R
index 65ee7e3..35290e5 100644
--- a/R/app.R
+++ b/R/app.R
@@ -27,6 +27,12 @@
 #'   request to determine whether the \code{ui} should be used to handle the
 #'   request. Note that the entire request path must match the regular
 #'   expression in order for the match to be considered successful.
+#' @param enableBookmarking Can be one of \code{"url"}, \code{"server"}, or
+#'   \code{"disable"}. This is equivalent to calling the
+#'   \code{\link{enableBookmarking}()} function just before calling
+#'   \code{shinyApp()}. With the default value (\code{NULL}), the app will
+#'   respect the setting from any previous calls to \code{enableBookmarking()}.
+#'   See \code{\link{enableBookmarking}} for more information.
 #' @return An object that represents the app. Printing the object or passing it
 #'   to \code{\link{runApp}} will run the app.
 #'
@@ -59,10 +65,9 @@
 #'
 #'   runApp(app)
 #' }
-#'
 #' @export
 shinyApp <- function(ui=NULL, server=NULL, onStart=NULL, options=list(),
-                     uiPattern="/") {
+                     uiPattern="/", enableBookmarking = NULL) {
   if (is.null(server)) {
     stop("`server` missing from shinyApp")
   }
@@ -76,12 +81,24 @@ shinyApp <- function(ui=NULL, server=NULL, onStart=NULL, options=list(),
     server
   }
 
+  if (!is.null(enableBookmarking)) {
+    bookmarkStore <- match.arg(enableBookmarking, c("url", "server", "disable"))
+    enableBookmarking(bookmarkStore)
+  }
+
+  # Store the appDir and bookmarking-related options, so that we can read them
+  # from within the app.
+  shinyOptions(appDir = getwd())
+  appOptions <- consumeAppOptions()
+
   structure(
     list(
       httpHandler = httpHandler,
       serverFuncSource = serverFuncSource,
       onStart = onStart,
-      options = options),
+      options = options,
+      appOptions = appOptions
+    ),
     class = "shiny.appobj"
   )
 }
@@ -113,7 +130,9 @@ shinyAppDir <- function(appDir, options=list()) {
 #' @export
 shinyAppFile <- function(appFile, options=list()) {
   appFile <- normalizePath(appFile, mustWork = TRUE)
-  shinyAppDir_appR(basename(appFile), dirname(appFile), options = options)
+  appDir <- dirname(appFile)
+
+  shinyAppDir_appR(basename(appFile), appDir, options = options)
 }
 
 # This reads in an app dir in the case that there's a server.R (and ui.R/www)
@@ -178,6 +197,8 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
     }
   }
 
+  shinyOptions(appDir = appDir)
+
   oldwd <- NULL
   monitorHandle <- NULL
   onStart <- function() {
@@ -199,7 +220,8 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
       serverFuncSource = serverFuncSource,
       onStart = onStart,
       onEnd = onEnd,
-      options = options),
+      options = options
+    ),
     class = "shiny.appobj"
   )
 }
@@ -209,13 +231,13 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
 # ignored when checking extensions. If any changes are detected, all connected
 # Shiny sessions are reloaded.
 #
-# Use option(shiny.autoreload = TRUE) to enable this behavior. Since monitoring
+# Use options(shiny.autoreload = TRUE) to enable this behavior. Since monitoring
 # for changes is expensive (we are polling for mtimes here, nothing fancy) this
 # feature is intended only for development.
 #
 # You can customize the file patterns Shiny will monitor by setting the
 # shiny.autoreload.pattern option. For example, to monitor only ui.R:
-# option(shiny.autoreload.pattern = glob2rx("ui.R"))
+# options(shiny.autoreload.pattern = glob2rx("ui.R"))
 #
 # The return value is a function that halts monitoring when called.
 initAutoReloadMonitor <- function(dir) {
@@ -252,7 +274,8 @@ initAutoReloadMonitor <- function(dir) {
 
 # This reads in an app dir for a single-file application (e.g. app.R), and
 # returns a shiny.appobj.
-shinyAppDir_appR <- function(fileName, appDir, options=list()) {
+shinyAppDir_appR <- function(fileName, appDir, options=list())
+{
   fullpath <- file.path.ci(appDir, fileName)
 
   # This sources app.R and caches the content. When appObj() is called but
@@ -265,6 +288,8 @@ shinyAppDir_appR <- function(fileName, appDir, options=list()) {
       if (!is.shiny.appobj(result))
         stop("app.R did not return a shiny.appobj object.")
 
+      unconsumeAppOptions(result$appOptions)
+
       return(result)
     }
   )
diff --git a/R/bookmark-state-local.R b/R/bookmark-state-local.R
new file mode 100644
index 0000000..856b258
--- /dev/null
+++ b/R/bookmark-state-local.R
@@ -0,0 +1,28 @@
+# Function wrappers for saving and restoring state to/from disk when running
+# Shiny locally.
+#
+# These functions provide a directory to the callback function.
+#
+# @param id A session ID to save.
+# @param callback A callback function that saves state to or restores state from
+#   a directory. It must take one argument, \code{stateDir}, which is a
+#   directory to which it writes/reads.
+
+saveInterfaceLocal <- function(id, callback) {
+  # Try to save in app directory
+  appDir <- getShinyOption("appDir", default = getwd())
+
+  stateDir <- file.path(appDir, "shiny_bookmarks", id)
+  if (!dirExists(stateDir))
+    dir.create(stateDir, recursive = TRUE)
+
+  callback(stateDir)
+}
+
+loadInterfaceLocal <- function(id, callback) {
+  # Try to load from app directory
+  appDir <- getShinyOption("appDir", default = getwd())
+
+  stateDir <- file.path(appDir, "shiny_bookmarks", id)
+  callback(stateDir)
+}
diff --git a/R/bookmark-state.R b/R/bookmark-state.R
new file mode 100644
index 0000000..cc3c46e
--- /dev/null
+++ b/R/bookmark-state.R
@@ -0,0 +1,1113 @@
+#' @include stack.R
+NULL
+
+ShinySaveState <- R6Class("ShinySaveState",
+  public = list(
+    input = NULL,
+    exclude = NULL,
+    onSave = NULL, # A callback to invoke during the saving process.
+
+    # These are set not in initialize(), but by external functions that modify
+    # the ShinySaveState object.
+    dir = NULL,
+
+
+    initialize = function(input = NULL, exclude = NULL, onSave = NULL) {
+      self$input   <- input
+      self$exclude <- exclude
+      self$onSave  <- onSave
+      private$values_  <- new.env(parent = emptyenv())
+    }
+  ),
+
+  active = list(
+    # `values` looks to the outside world like an environment for storing
+    # arbitrary values. Two things to note: (1) This is an environment (instead
+    # of, say, a list) because if the onSave function represents multiple
+    # callback functions (when onBookmark is called multiple times), each
+    # callback can change `values`, and if we used a list, one of the callbacks
+    # could easily obliterate values set by another. This can happen when using
+    # modules that have an onBookmark function. (2) The purpose of the active
+    # binding is to prevent replacing state$values with another arbitrary
+    # object. (Simply locking the binding would prevent all changes to
+    # state$values.)
+    values = function(value) {
+      if (missing(value))
+        return(private$values_)
+
+      if (identical(value, private$values_)) {
+        return(value)
+      } else {
+        stop("Items in `values` can be changed, but `values` itself cannot be replaced.")
+      }
+    }
+  ),
+
+  private = list(
+    values_ = NULL
+  )
+)
+
+
+# Save a state to disk. Returns a query string which can be used to restore the
+# session.
+saveShinySaveState <- function(state) {
+  id <- createUniqueId(8)
+
+  # A function for saving the state object to disk, given a directory to save
+  # to.
+  saveState <- function(stateDir) {
+    state$dir <- stateDir
+
+    # Allow user-supplied onSave function to do things like add state$values, or
+    # save data to state dir.
+    if (!is.null(state$onSave))
+      isolate(state$onSave(state))
+
+    # Serialize values, possibly saving some extra data to stateDir
+    exclude <- c(state$exclude, "._bookmark_")
+    inputValues <- serializeReactiveValues(state$input, exclude, state$dir)
+    saveRDS(inputValues, file.path(stateDir, "input.rds"))
+
+    # If values were added, save them also.
+    if (length(state$values) != 0)
+      saveRDS(state$values, file.path(stateDir, "values.rds"))
+  }
+
+  # Pass the saveState function to the save interface function, which will
+  # invoke saveState after preparing the directory.
+
+  # Look for a save.interface function. This will be defined by the hosting
+  # environment if it supports bookmarking.
+  saveInterface <- getShinyOption("save.interface")
+
+  if (is.null(saveInterface)) {
+    if (inShinyServer()) {
+      # We're in a version of Shiny Server/Connect that doesn't have
+      # bookmarking support.
+      saveInterface <- function(id, callback) {
+        stop("The hosting environment does not support saved-to-server bookmarking.")
+      }
+
+    } else {
+      # We're running Shiny locally.
+      saveInterface <- saveInterfaceLocal
+    }
+  }
+
+  saveInterface(id, saveState)
+
+  paste0("_state_id_=", encodeURIComponent(id))
+}
+
+# Encode the state to a URL. This does not save to disk.
+encodeShinySaveState <- function(state) {
+  exclude <- c(state$exclude, "._bookmark_")
+  inputVals <- serializeReactiveValues(state$input, exclude, stateDir = NULL)
+
+  # Allow user-supplied onSave function to do things like add state$values.
+  if (!is.null(state$onSave))
+    isolate(state$onSave(state))
+
+  inputVals <- vapply(inputVals,
+    function(x) toJSON(x, strict_atomic = FALSE),
+    character(1),
+    USE.NAMES = TRUE
+  )
+
+  res <- ""
+
+  # If any input values are present, add them.
+  if (length(inputVals) != 0) {
+    res <- paste0(res, "_inputs_&",
+      paste0(
+        encodeURIComponent(names(inputVals)),
+        "=",
+        encodeURIComponent(inputVals),
+        collapse = "&"
+      )
+    )
+  }
+
+  # If 'values' is present, add them as well.
+  if (length(state$values) != 0) {
+    values <- vapply(state$values,
+      function(x) toJSON(x, strict_atomic = FALSE),
+      character(1),
+      USE.NAMES = TRUE
+    )
+
+    res <- paste0(res,
+      if (length(inputVals != 0)) "&",  # Add separator if there were inputs
+      "_values_&",
+      paste0(
+        encodeURIComponent(names(values)),
+        "=",
+        encodeURIComponent(values),
+        collapse = "&"
+      )
+    )
+  }
+
+  res
+}
+
+RestoreContext <- R6Class("RestoreContext",
+  public = list(
+    # This will be set to TRUE if there's actually a state to restore
+    active = FALSE,
+
+    # This is set to an error message string in case there was an initialization
+    # error. Later, after the app has started on the client, the server can send
+    # this message as a notification on the client.
+    initErrorMessage = NULL,
+
+    # This is a RestoreInputSet for input values. This is a key-value store with
+    # some special handling.
+    input = NULL,
+
+    # Directory for extra files, if restoring from state that was saved to disk.
+    dir = NULL,
+
+    # For values other than input values. These values don't need the special
+    # phandling that's needed for input values, because they're only accessed
+    # from the onRestore function.
+    values = NULL,
+
+    initialize = function(queryString = NULL) {
+      self$reset() # Need this to initialize self$input
+
+      if (!is.null(queryString) && nzchar(queryString)) {
+        tryCatch(
+          withLogErrors({
+            qsValues <- parseQueryString(queryString, nested = TRUE)
+
+            if (!is.null(qsValues[["__subapp__"]]) && qsValues[["__subapp__"]] == 1) {
+              # Ignore subapps in shiny docs
+              self$reset()
+
+            } else if (!is.null(qsValues[["_state_id_"]]) && nzchar(qsValues[["_state_id_"]])) {
+              # If we have a "_state_id_" key, restore from saved state and
+              # ignore other key/value pairs. If not, restore from key/value
+              # pairs in the query string.
+              self$active <- TRUE
+              private$loadStateQueryString(queryString)
+
+            } else {
+              # The query string contains the saved keys and values
+              self$active <- TRUE
+              private$decodeStateQueryString(queryString)
+            }
+          }),
+          error = function(e) {
+            # If there's an error in restoring problem, just reset these values
+            self$reset()
+            self$initErrorMessage <- e$message
+            warning(e$message)
+          }
+        )
+      }
+    },
+
+    reset = function() {
+      self$active <- FALSE
+      self$initErrorMessage <- NULL
+      self$input <- RestoreInputSet$new(list())
+      self$values <- new.env(parent = emptyenv())
+      self$dir <- NULL
+    },
+
+    # This should be called before a restore context is popped off the stack.
+    flushPending = function() {
+      self$input$flushPending()
+    },
+
+
+    # Returns a list representation of the RestoreContext object. This is passed
+    # to the app author's onRestore function. An important difference between
+    # the RestoreContext object and the list is that the former's `input` field
+    # is a RestoreInputSet object, while the latter's `input` field is just a
+    # list.
+    asList = function() {
+      list(
+        input = self$input$asList(),
+        dir = self$dir,
+        values = self$values
+      )
+    }
+  ),
+
+  private = list(
+    # Given a query string with a _state_id_, load saved state with that ID.
+    loadStateQueryString = function(queryString) {
+      values <- parseQueryString(queryString, nested = TRUE)
+      id <- values[["_state_id_"]]
+
+      # Check that id has only alphanumeric chars
+      if (grepl("[^a-zA-Z0-9]", id)) {
+        stop("Invalid state id: ", id)
+      }
+
+      # This function is passed to the loadInterface function; given a
+      # directory, it will load state from that directory
+      loadFun <- function(stateDir) {
+        self$dir <- stateDir
+
+        if (!dirExists(stateDir)) {
+          stop("Bookmarked state directory does not exist.")
+        }
+
+        tryCatch({
+            inputValues <- readRDS(file.path(stateDir, "input.rds"))
+            self$input <- RestoreInputSet$new(inputValues)
+          },
+          error = function(e) {
+            stop("Error reading input values file.")
+          }
+        )
+
+        valuesFile <- file.path(stateDir, "values.rds")
+        if (file.exists(valuesFile)) {
+          tryCatch({
+              self$values <- readRDS(valuesFile)
+            },
+            error = function(e) {
+              stop("Error reading values file.")
+            }
+          )
+        }
+      }
+
+      # Look for a load.interface function. This will be defined by the hosting
+      # environment if it supports bookmarking.
+      loadInterface <- getShinyOption("load.interface")
+
+      if (is.null(loadInterface)) {
+        if (inShinyServer()) {
+          # We're in a version of Shiny Server/Connect that doesn't have
+          # bookmarking support.
+          loadInterface <- function(id, callback) {
+            stop("The hosting environment does not support saved-to-server bookmarking.")
+          }
+
+        } else {
+          # We're running Shiny locally.
+          loadInterface <- loadInterfaceLocal
+        }
+      }
+
+      loadInterface(id, loadFun)
+
+      invisible()
+    },
+
+    # Given a query string with values encoded in it, restore saved state
+    # from those values.
+    decodeStateQueryString = function(queryString) {
+      # Remove leading '?'
+      if (substr(queryString, 1, 1) == '?')
+        queryString <- substr(queryString, 2, nchar(queryString))
+
+
+      # Error if multiple '_inputs_' or '_values_'. This is needed because
+      # strsplit won't add an entry if the search pattern is at the end of a
+      # string.
+      if (length(gregexpr("(^|&)_inputs_(&|$)", queryString)[[1]]) > 1)
+        stop("Invalid state string: more than one '_inputs_' found")
+      if (length(gregexpr("(^|&)_values_(&|$)", queryString)[[1]]) > 1)
+        stop("Invalid state string: more than one '_values_' found")
+
+      # Look for _inputs_ and store following content in inputStr
+      splitStr <- strsplit(queryString, "(^|&)_inputs_(&|$)")[[1]]
+      if (length(splitStr) == 2) {
+        inputStr <- splitStr[2]
+        # Remove any _values_ (and content after _values_) that may come after
+        # _inputs_
+        inputStr <- strsplit(inputStr, "(^|&)_values_(&|$)")[[1]][1]
+
+      } else {
+        inputStr <- ""
+      }
+
+      # Look for _values_ and store following content in valueStr
+      splitStr <- strsplit(queryString, "(^|&)_values_(&|$)")[[1]]
+      if (length(splitStr) == 2) {
+        valueStr <- splitStr[2]
+        # Remove any _inputs_ (and content after _inputs_) that may come after
+        # _values_
+        valueStr <- strsplit(valueStr, "(^|&)_inputs_(&|$)")[[1]][1]
+
+      } else {
+        valueStr <- ""
+      }
+
+
+      inputs <- parseQueryString(inputStr, nested = TRUE)
+      values <- parseQueryString(valueStr, nested = TRUE)
+
+      valuesFromJSON <- function(vals) {
+        mapply(names(vals), vals, SIMPLIFY = FALSE,
+          FUN = function(name, value) {
+            tryCatch(
+              jsonlite::fromJSON(value),
+              error = function(e) {
+                stop("Failed to parse URL parameter \"", name, "\"")
+              }
+            )
+          }
+        )
+      }
+
+      inputs <- valuesFromJSON(inputs)
+      self$input <- RestoreInputSet$new(inputs)
+
+      values <- valuesFromJSON(values)
+      self$values <- list2env2(values, self$values)
+    }
+  )
+)
+
+
+# Restore input set. This is basically a key-value store, except for one
+# important difference: When the user `get()`s a value, the value is marked as
+# pending; when `flushPending()` is called, those pending values are marked as
+# used. When a value is marked as used, `get()` will not return it, unless
+# called with `force=TRUE`. This is to make sure that a particular value can be
+# restored only within a single call to `withRestoreContext()`. Without this, if
+# a value is restored in a dynamic UI, it could completely prevent any other
+# (non- restored) kvalue from being used.
+RestoreInputSet <- R6Class("RestoreInputSet",
+  private = list(
+    values = NULL,
+    pending = character(0),
+    used = character(0)     # Names of values which have been used
+  ),
+
+  public = list(
+    initialize = function(values) {
+      private$values <- list2env2(values, parent = emptyenv())
+    },
+
+    exists = function(name) {
+      exists(name, envir = private$values)
+    },
+
+    # Return TRUE if the value exists and has not been marked as used.
+    available = function(name) {
+      self$exists(name) && !self$isUsed(name)
+    },
+
+    isPending = function(name) {
+      name %in% private$pending
+    },
+
+    isUsed = function(name) {
+      name %in% private$used
+    },
+
+    # Get a value. If `force` is TRUE, get the value without checking whether
+    # has been used, and without marking it as pending.
+    get = function(name, force = FALSE) {
+      if (force)
+        return(private$values[[name]])
+
+      if (!self$available(name))
+        return(NULL)
+
+      # Mark this name as pending. Use unique so that it's not added twice.
+      private$pending <- unique(c(private$pending, name))
+      private$values[[name]]
+    },
+
+    # Take pending names and mark them as used, then clear pending list.
+    flushPending = function() {
+      private$used <- unique(c(private$used, private$pending))
+      private$pending <- character(0)
+    },
+
+    asList = function() {
+      as.list.environment(private$values)
+    }
+  )
+)
+
+
+restoreCtxStack <- Stack$new()
+
+withRestoreContext <- function(ctx, expr) {
+  restoreCtxStack$push(ctx)
+
+  on.exit({
+    # Mark pending names as used
+    restoreCtxStack$peek()$flushPending()
+    restoreCtxStack$pop()
+  }, add = TRUE)
+
+  force(expr)
+}
+
+# Is there a current restore context?
+hasCurrentRestoreContext <- function() {
+  restoreCtxStack$size() > 0
+}
+
+# Call to access the current restore context
+getCurrentRestoreContext <- function() {
+  ctx <- restoreCtxStack$peek()
+  if (is.null(ctx)) {
+    stop("No restore context found")
+  }
+  ctx
+}
+
+#' Restore an input value
+#'
+#' This restores an input value from the current restore context. It should be
+#' called early on inside of input functions (like \code{\link{textInput}}).
+#'
+#' @param id Name of the input value to restore.
+#' @param default A default value to use, if there's no value to restore.
+#'
+#' @export
+restoreInput <- function(id, default) {
+  # Need to evaluate `default` in case it contains reactives like input$x. If we
+  # don't, then the calling code won't take a reactive dependency on input$x
+  # when restoring a value.
+  force(default)
+
+  if (!hasCurrentRestoreContext()) {
+    return(default)
+  }
+
+  oldInputs <- getCurrentRestoreContext()$input
+  if (oldInputs$available(id)) {
+    oldInputs$get(id)
+  } else {
+    default
+  }
+}
+
+#' Update URL in browser's location bar
+#'
+#' This function updates the client browser's query string in the location bar.
+#' It typically is called from an observer. Note that this will not work in
+#' Internet Explorer 9 and below.
+#'
+#' @param queryString The new query string to show in the location bar.
+#' @param session A Shiny session object.
+#' @seealso \code{\link{enableBookmarking}} for examples.
+#' @export
+updateQueryString <- function(queryString, session = getDefaultReactiveDomain()) {
+  session$updateQueryString(queryString)
+}
+
+#' Create a button for bookmarking/sharing
+#'
+#' A \code{bookmarkButton} is a \code{\link{actionButton}} with a default label
+#' that consists of a link icon and the text "Bookmark...". It is meant to be
+#' used for bookmarking state.
+#'
+#' @inheritParams actionButton
+#' @param title A tooltip that is shown when the mouse cursor hovers over the
+#'   button.
+#' @param id An ID for the bookmark button. The only time it is necessary to set
+#'   the ID unless you have more than one bookmark button in your application.
+#'   If you specify an input ID, it should be excluded from bookmarking with
+#'   \code{\link{setBookmarkExclude}}, and you must create an observer that
+#'   does the bookmarking when the button is pressed. See the examples below.
+#'
+#' @seealso \code{\link{enableBookmarking}} for more examples.
+#'
+#' @examples
+#' ## Only run these examples in interactive sessions
+#' if (interactive()) {
+#'
+#' # This example shows how to use multiple bookmark buttons. If you only need
+#' # a single bookmark button, see examples in ?enableBookmarking.
+#' ui <- function(request) {
+#'   fluidPage(
+#'     tabsetPanel(id = "tabs",
+#'       tabPanel("One",
+#'         checkboxInput("chk1", "Checkbox 1"),
+#'         bookmarkButton(id = "bookmark1")
+#'       ),
+#'       tabPanel("Two",
+#'         checkboxInput("chk2", "Checkbox 2"),
+#'         bookmarkButton(id = "bookmark2")
+#'       )
+#'     )
+#'   )
+#' }
+#' server <- function(input, output, session) {
+#'   # Need to exclude the buttons from themselves being bookmarked
+#'   setBookmarkExclude(c("bookmark1", "bookmark2"))
+#'
+#'   # Trigger bookmarking with either button
+#'   observeEvent(input$bookmark1, {
+#'     session$doBookmark()
+#'   })
+#'   observeEvent(input$bookmark2, {
+#'     session$doBookmark()
+#'   })
+#' }
+#' enableBookmarking(store = "url")
+#' shinyApp(ui, server)
+#' }
+#' @export
+bookmarkButton <- function(label = "Bookmark...",
+  icon = shiny::icon("link", lib = "glyphicon"),
+  title = "Bookmark this application's state and get a URL for sharing.",
+  ...,
+  id = "._bookmark_")
+{
+  actionButton(id, label, icon, title = title, ...)
+}
+
+
+#' Generate a modal dialog that displays a URL
+#'
+#' The modal dialog generated by \code{urlModal} will display the URL in a
+#' textarea input, and the URL text will be selected so that it can be easily
+#' copied. The result from \code{urlModal} should be passed to the
+#' \code{\link{showModal}} function to display it in the browser.
+#'
+#' @param url A URL to display in the dialog box.
+#' @param title A title for the dialog box.
+#' @param subtitle Text to display underneath URL.
+#' @export
+urlModal <- function(url, title = "Bookmarked application link", subtitle = NULL) {
+
+  subtitleTag <- tagList(
+    br(),
+    span(class = "text-muted", subtitle),
+    span(id = "shiny-bookmark-copy-text", class = "text-muted")
+  )
+
+  modalDialog(
+    title = title,
+    easyClose = TRUE,
+    tags$textarea(class = "form-control", rows = "1", style = "resize: none;",
+      readonly = "readonly",
+      url
+    ),
+    subtitleTag,
+    # Need separate show and shown listeners. The show listener sizes the
+    # textarea just as the modal starts to fade in. The 200ms delay is needed
+    # because if we try to resize earlier, it can't calculate the text height
+    # (scrollHeight will be reported as zero). The shown listener selects the
+    # text; it's needed because because selection has to be done after the fade-
+    # in is completed.
+    tags$script(
+      "$('#shiny-modal').
+        one('show.bs.modal', function() {
+          setTimeout(function() {
+            var $textarea = $('#shiny-modal textarea');
+            $textarea.innerHeight($textarea[0].scrollHeight);
+          }, 200);
+        });
+      $('#shiny-modal')
+        .one('shown.bs.modal', function() {
+          $('#shiny-modal textarea').select().focus();
+        });
+      $('#shiny-bookmark-copy-text')
+        .text(function() {
+          if (/Mac/i.test(navigator.userAgent)) {
+            return 'Press \u2318-C to copy.';
+          } else {
+            return 'Press Ctrl-C to copy.';
+          }
+        });
+      "
+    )
+  )
+}
+
+
+#' Display a modal dialog for bookmarking
+#'
+#' This is a wrapper function for \code{\link{urlModal}} that is automatically
+#' called if an application is bookmarked but no other \code{\link{onBookmark}}
+#' callback was set. It displays a modal dialog with the bookmark URL, along
+#' with a subtitle that is appropriate for the type of bookmarking used ("url"
+#' or "server").
+#'
+#' @param url A URL to show in the modal dialog.
+#' @export
+showBookmarkUrlModal <- function(url) {
+  store <- getShinyOption("bookmarkStore", default = "")
+  if (store == "url") {
+    subtitle <- "This link stores the current state of this application."
+  } else if (store == "server") {
+    subtitle <- "The current state of this application has been stored on the server."
+  } else {
+    subtitle <- NULL
+  }
+
+  showModal(urlModal(url, subtitle = subtitle))
+}
+
+#' Enable bookmarking for a Shiny application
+#'
+#' @description
+#'
+#' There are two types of bookmarking: saving an application's state to disk on
+#' the server, and encoding the application's state in a URL. For state that has
+#' been saved to disk, the state can be restored with the corresponding state
+#' ID. For URL-encoded state, the state of the application is encoded in the
+#' URL, and no server-side storage is needed.
+#'
+#' URL-encoded bookmarking is appropriate for applications where there not many
+#' input values that need to be recorded. Some browsers have a length limit for
+#' URLs of about 2000 characters, and if there are many inputs, the length of
+#' the URL can exceed that limit.
+#'
+#' Saved-on-server bookmarking is appropriate when there are many inputs, or
+#' when the bookmarked state requires storing files.
+#'
+#' @details
+#'
+#' For restoring state to work properly, the UI must be a function that takes
+#' one argument, \code{request}. In most Shiny applications, the UI is not a
+#' function; it might have the form \code{fluidPage(....)}. Converting it to a
+#' function is as simple as wrapping it in a function, as in
+#' \code{function(request) \{ fluidPage(....) \}}.
+#'
+#' By default, all input values will be bookmarked, except for the values of
+#' passwordInputs. fileInputs will be saved if the state is saved on a server,
+#' but not if the state is encoded in a URL.
+#'
+#' When bookmarking state, arbitrary values can be stored, by passing a function
+#' as the \code{onBookmark} argument. That function will be passed a
+#' \code{ShinySaveState} object. The \code{values} field of the object is a list
+#' which can be manipulated to save extra information. Additionally, if the
+#' state is being saved on the server, and the \code{dir} field of that object
+#' can be used to save extra information to files in that directory.
+#'
+#' For saved-to-server state, this is how the state directory is chosen:
+#' \itemize{
+#'   \item If running in a hosting environment such as Shiny Server or
+#'     Connect, the hosting environment will choose the directory.
+#'   \item If running an app in a directory with \code{\link{runApp}()}, the
+#'     saved states will be saved in a subdirectory of the app called
+#'    shiny_bookmarks.
+#'   \item If running a Shiny app object that is generated from code (not run
+#'     from a directory), the saved states will be saved in a subdirectory of
+#'     the current working directory called shiny_bookmarks.
+#' }
+#'
+#' When used with \code{\link{shinyApp}()}, this function must be called before
+#' \code{shinyApp()}, or in the \code{shinyApp()}'s \code{onStart} function. An
+#' alternative to calling the \code{enableBookmarking()} function is to use the
+#' \code{enableBookmarking} \emph{argument} for \code{shinyApp()}. See examples
+#' below.
+#'
+#' @param store Either \code{"url"}, which encodes all of the relevant values in
+#'   a URL, \code{"server"}, which saves to disk on the server, or
+#'   \code{"disable"}, which disables any previously-enabled bookmarking.
+#'
+#' @seealso \code{\link{onBookmark}}, \code{\link{onBookmarked}},
+#'   \code{\link{onRestore}}, and \code{\link{onRestored}} for registering
+#'   callback functions that are invoked when the state is bookmarked or
+#'   restored.
+#'
+#'   Also see \code{\link{updateQueryString}}.
+#'
+#' @export
+#' @examples
+#' ## Only run these examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' # Basic example with state encoded in URL
+#' ui <- function(request) {
+#'   fluidPage(
+#'     textInput("txt", "Text"),
+#'     checkboxInput("chk", "Checkbox"),
+#'     bookmarkButton()
+#'   )
+#' }
+#' server <- function(input, output, session) { }
+#' enableBookmarking("url")
+#' shinyApp(ui, server)
+#'
+#'
+#' # An alternative to calling enableBookmarking(): use shinyApp's
+#' # enableBookmarking argument
+#' shinyApp(ui, server, enableBookmarking = "url")
+#'
+#'
+#' # Same basic example with state saved to disk
+#' enableBookmarking("server")
+#' shinyApp(ui, server)
+#'
+#'
+#' # Save/restore arbitrary values
+#' ui <- function(req) {
+#'   fluidPage(
+#'     textInput("txt", "Text"),
+#'     checkboxInput("chk", "Checkbox"),
+#'     bookmarkButton(),
+#'     br(),
+#'     textOutput("lastSaved")
+#'   )
+#' }
+#' server <- function(input, output, session) {
+#'   vals <- reactiveValues(savedTime = NULL)
+#'   output$lastSaved <- renderText({
+#'     if (!is.null(vals$savedTime))
+#'       paste("Last saved at", vals$savedTime)
+#'     else
+#'       ""
+#'   })
+#'
+#'   onBookmark(function(state) {
+#'     vals$savedTime <- Sys.time()
+#'     # state is a mutable reference object, and we can add arbitrary values
+#'     # to it.
+#'     state$values$time <- vals$savedTime
+#'   })
+#'   onRestore(function(state) {
+#'     vals$savedTime <- state$values$time
+#'   })
+#' }
+#' enableBookmarking(store = "url")
+#' shinyApp(ui, server)
+#'
+#'
+#' # Usable with dynamic UI (set the slider, then change the text input,
+#' # click the bookmark button)
+#' ui <- function(request) {
+#'   fluidPage(
+#'     sliderInput("slider", "Slider", 1, 100, 50),
+#'     uiOutput("ui"),
+#'     bookmarkButton()
+#'   )
+#' }
+#' server <- function(input, output, session) {
+#'   output$ui <- renderUI({
+#'     textInput("txt", "Text", input$slider)
+#'   })
+#' }
+#' enableBookmarking("url")
+#' shinyApp(ui, server)
+#'
+#'
+#' # Exclude specific inputs (The only input that will be saved in this
+#' # example is chk)
+#' ui <- function(request) {
+#'   fluidPage(
+#'     passwordInput("pw", "Password"), # Passwords are never saved
+#'     sliderInput("slider", "Slider", 1, 100, 50), # Manually excluded below
+#'     checkboxInput("chk", "Checkbox"),
+#'     bookmarkButton()
+#'   )
+#' }
+#' server <- function(input, output, session) {
+#'   setBookmarkExclude("slider")
+#' }
+#' enableBookmarking("url")
+#' shinyApp(ui, server)
+#'
+#'
+#' # Update the browser's location bar every time an input changes. This should
+#' # not be used with enableBookmarking("server"), because that would create a
+#' # new saved state on disk every time the user changes an input.
+#' ui <- function(req) {
+#'   fluidPage(
+#'     textInput("txt", "Text"),
+#'     checkboxInput("chk", "Checkbox")
+#'   )
+#' }
+#' server <- function(input, output, session) {
+#'   observe({
+#'     # Trigger this observer every time an input changes
+#'     reactiveValuesToList(input)
+#'     session$doBookmark()
+#'   })
+#'   onBookmarked(function(url) {
+#'     updateQueryString(url)
+#'   })
+#' }
+#' enableBookmarking("url")
+#' shinyApp(ui, server)
+#'
+#'
+#' # Save/restore uploaded files
+#' ui <- function(request) {
+#'   fluidPage(
+#'     sidebarLayout(
+#'       sidebarPanel(
+#'         fileInput("file1", "Choose CSV File", multiple = TRUE,
+#'           accept = c(
+#'             "text/csv",
+#'             "text/comma-separated-values,text/plain",
+#'             ".csv"
+#'           )
+#'         ),
+#'         tags$hr(),
+#'         checkboxInput("header", "Header", TRUE),
+#'         bookmarkButton()
+#'       ),
+#'       mainPanel(
+#'         tableOutput("contents")
+#'       )
+#'     )
+#'   )
+#' }
+#' server <- function(input, output) {
+#'   output$contents <- renderTable({
+#'     inFile <- input$file1
+#'     if (is.null(inFile))
+#'       return(NULL)
+#'
+#'     if (nrow(inFile) == 1) {
+#'       read.csv(inFile$datapath, header = input$header)
+#'     } else {
+#'       data.frame(x = "multiple files")
+#'     }
+#'   })
+#' }
+#' enableBookmarking("server")
+#' shinyApp(ui, server)
+#'
+#' }
+enableBookmarking <- function(store = c("url", "server", "disable")) {
+  store <- match.arg(store)
+  shinyOptions(bookmarkStore = store)
+}
+
+
+#' Exclude inputs from bookmarking
+#'
+#' This function tells Shiny which inputs should be excluded from bookmarking.
+#' It should be called from inside the application's server function.
+#'
+#' This function can also be called from a module's server function, in which
+#' case it will exclude inputs with the specified names, from that module. It
+#' will not affect inputs from other modules or from the top level of the Shiny
+#' application.
+#'
+#' @param names A character vector containing names of inputs to exclude from
+#'   bookmarking.
+#' @param session A shiny session object.
+#' @seealso \code{\link{enableBookmarking}} for examples.
+#' @export
+setBookmarkExclude <- function(names = character(0), session = getDefaultReactiveDomain()) {
+  session$setBookmarkExclude(names)
+}
+
+
+#' Add callbacks for Shiny session bookmarking events
+#'
+#' @description
+#'
+#' These functions are for registering callbacks on Shiny session events. They
+#' should be called within an application's server function.
+#'
+#' \itemize{
+#'   \item \code{onBookmark} registers a function that will be called just
+#'     before Shiny bookmarks state.
+#'   \item \code{onBookmarked} registers a function that will be called just
+#'     after Shiny bookmarks state.
+#'   \item \code{onRestore} registers a function that will be called when a
+#'     session is restored, after the server function executes, but before all
+#'     other reactives, observers and render functions are run.
+#'   \item \code{onRestored} registers a function that will be called after a
+#'     session is restored. This is similar to \code{onRestore}, but it will be
+#'     called after all reactives, observers, and render functions run, and
+#'     after results are sent to the client browser. \code{onRestored}
+#'     callbacks can be useful for sending update messages to the client
+#'     browser.
+#' }
+#'
+#' @details
+#'
+#' All of these functions return a function which can be called with no
+#' arguments to cancel the registration.
+#'
+#' The callback function that is passed to these functions should take one
+#' argument, typically named "state" (for \code{onBookmark}, \code{onRestore},
+#' and \code{onRestored}) or "url" (for \code{onBookmarked}).
+#'
+#' For \code{onBookmark}, the state object has three relevant fields. The
+#' \code{values} field is an environment which can be used to save arbitrary
+#' values (see examples). If the state is being saved to disk (as opposed to
+#' being encoded in a URL), the \code{dir} field contains the name of a
+#' directory which can be used to store extra files. Finally, the state object
+#' has an \code{input} field, which is simply the application's \code{input}
+#' object. It can be read, but not modified.
+#'
+#' For \code{onRestore} and \code{onRestored}, the state object is a list. This
+#' list contains \code{input}, which is a named list of input values to restore,
+#' \code{values}, which is an environment containing arbitrary values that were
+#' saved in \code{onBookmark}, and \code{dir}, the name of the directory that
+#' the state is being restored from, and which could have been used to save
+#' extra files.
+#'
+#' For \code{onBookmarked}, the callback function receives a string with the
+#' bookmark URL. This callback function should be used to display UI in the
+#' client browser with the bookmark URL. If no callback function is registered,
+#' then Shiny will by default display a modal dialog with the bookmark URL.
+#'
+#' @section Modules:
+#'
+#'   These callbacks may also be used in Shiny modules. When used this way, the
+#'   inputs and values will automatically be namespaced for the module, and the
+#'   callback functions registered for the module will only be able to see the
+#'   module's inputs and values.
+#'
+#' @param fun A callback function which takes one argument.
+#' @param session A shiny session object.
+#' @seealso enableBookmarking for general information on bookmarking.
+#'
+#' @examples
+#' ## Only run these examples in interactive sessions
+#' if (interactive()) {
+#'
+#' # Basic use of onBookmark and onRestore: This app saves the time in its
+#' # arbitrary values, and restores that time when the app is restored.
+#' ui <- function(req) {
+#'   fluidPage(
+#'     textInput("txt", "Input text"),
+#'     bookmarkButton()
+#'   )
+#' }
+#' server <- function(input, output) {
+#'   onBookmark(function(state) {
+#'     savedTime <- as.character(Sys.time())
+#'     cat("Last saved at", savedTime, "\n")
+#'     # state is a mutable reference object, and we can add arbitrary values to
+#'     # it.
+#'     state$values$time <- savedTime
+#'   })
+#'
+#'   onRestore(function(state) {
+#'     cat("Restoring from state bookmarked at", state$values$time, "\n")
+#'   })
+#' }
+#' enableBookmarking("url")
+#' shinyApp(ui, server)
+#'
+#'
+#'
+# This app illustrates two things: saving values in a file using state$dir, and
+# using an onRestored callback to call an input updater function. (In real use
+# cases, it probably makes sense to save content to a file only if it's much
+# larger.)
+#' ui <- function(req) {
+#'   fluidPage(
+#'     textInput("txt", "Input text"),
+#'     bookmarkButton()
+#'   )
+#' }
+#' server <- function(input, output, session) {
+#'   lastUpdateTime <- NULL
+#'
+#'   observeEvent(input$txt, {
+#'     updateTextInput(session, "txt",
+#'       label = paste0("Input text (Changed ", as.character(Sys.time()), ")")
+#'     )
+#'   })
+#'
+#'   onBookmark(function(state) {
+#'     # Save content to a file
+#'     messageFile <- file.path(state$dir, "message.txt")
+#'     cat(as.character(Sys.time()), file = messageFile)
+#'   })
+#'
+#'   onRestored(function(state) {
+#'     # Read the file
+#'     messageFile <- file.path(state$dir, "message.txt")
+#'     timeText <- readChar(messageFile, 1000)
+#'
+#'     # updateTextInput must be called in onRestored, as opposed to onRestore,
+#'     # because onRestored happens after the client browser is ready.
+#'     updateTextInput(session, "txt",
+#'       label = paste0("Input text (Changed ", timeText, ")")
+#'     )
+#'   })
+#' }
+#' # "server" bookmarking is needed for writing to disk.
+#' enableBookmarking("server")
+#' shinyApp(ui, server)
+#'
+#'
+#' # This app has a module, and both the module and the main app code have
+#' # onBookmark and onRestore functions which write and read state$values$hash. The
+#' # module's version of state$values$hash does not conflict with the app's version
+#' # of state$values$hash.
+#' #
+#' # A basic module that captializes text.
+#' capitalizerUI <- function(id) {
+#'   ns <- NS(id)
+#'   wellPanel(
+#'     h4("Text captializer module"),
+#'     textInput(ns("text"), "Enter text:"),
+#'     verbatimTextOutput(ns("out"))
+#'   )
+#' }
+#' capitalizerServer <- function(input, output, session) {
+#'   output$out <- renderText({
+#'     toupper(input$text)
+#'   })
+#'   onBookmark(function(state) {
+#'     state$values$hash <- digest::digest(input$text, "md5")
+#'   })
+#'   onRestore(function(state) {
+#'     if (identical(digest::digest(input$text, "md5"), state$values$hash)) {
+#'       message("Module's input text matches hash ", state$values$hash)
+#'     } else {
+#'       message("Module's input text does not match hash ", state$values$hash)
+#'     }
+#'   })
+#' }
+#' # Main app code
+#' ui <- function(request) {
+#'   fluidPage(
+#'     sidebarLayout(
+#'       sidebarPanel(
+#'         capitalizerUI("tc"),
+#'         textInput("text", "Enter text (not in module):"),
+#'         bookmarkButton()
+#'       ),
+#'       mainPanel()
+#'     )
+#'   )
+#' }
+#' server <- function(input, output, session) {
+#'   callModule(capitalizerServer, "tc")
+#'   onBookmark(function(state) {
+#'     state$values$hash <- digest::digest(input$text, "md5")
+#'   })
+#'   onRestore(function(state) {
+#'     if (identical(digest::digest(input$text, "md5"), state$values$hash)) {
+#'       message("App's input text matches hash ", state$values$hash)
+#'     } else {
+#'       message("App's input text does not match hash ", state$values$hash)
+#'     }
+#'   })
+#' }
+#' enableBookmarking(store = "url")
+#' shinyApp(ui, server)
+#' }
+#' @export
+onBookmark <- function(fun, session = getDefaultReactiveDomain()) {
+  session$onBookmark(fun)
+}
+
+#' @rdname onBookmark
+#' @export
+onBookmarked <- function(fun, session = getDefaultReactiveDomain()) {
+  session$onBookmarked(fun)
+}
+
+#' @rdname onBookmark
+#' @export
+onRestore <- function(fun, session = getDefaultReactiveDomain()) {
+  session$onRestore(fun)
+}
+
+#' @rdname onBookmark
+#' @export
+onRestored <- function(fun, session = getDefaultReactiveDomain()) {
+  session$onRestored(fun)
+}
diff --git a/R/bootstrap-layout.R b/R/bootstrap-layout.R
index b9898ee..9588af1 100644
--- a/R/bootstrap-layout.R
+++ b/R/bootstrap-layout.R
@@ -31,7 +31,11 @@
 #' @seealso \code{\link{column}}, \code{\link{sidebarLayout}}
 #'
 #' @examples
-#' shinyUI(fluidPage(
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' # Example of UI with fluidPage
+#' ui <- fluidPage(
 #'
 #'   # Application title
 #'   titlePanel("Hello Shiny!"),
@@ -52,9 +56,21 @@
 #'       plotOutput("distPlot")
 #'     )
 #'   )
-#' ))
+#' )
+#'
+#' # Server logic
+#' server <- function(input, output) {
+#'   output$distPlot <- renderPlot({
+#'     hist(rnorm(input$obs))
+#'   })
+#' }
+#'
+#' # Complete app with UI and server components
+#' shinyApp(ui, server)
+#'
 #'
-#' shinyUI(fluidPage(
+#' # UI demonstrating column layouts
+#' ui <- fluidPage(
 #'   title = "Hello Shiny!",
 #'   fluidRow(
 #'     column(width = 4,
@@ -64,8 +80,10 @@
 #'       "3 offset 2"
 #'     )
 #'   )
-#' ))
+#' )
 #'
+#' shinyApp(ui, server = function(input, output) { })
+#' }
 #' @rdname fluidPage
 #' @export
 fluidPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
@@ -115,7 +133,10 @@ fluidRow <- function(...) {
 #' @seealso \code{\link{column}}
 #'
 #' @examples
-#' shinyUI(fixedPage(
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fixedPage(
 #'   title = "Hello, Shiny!",
 #'   fixedRow(
 #'     column(width = 4,
@@ -125,7 +146,10 @@ fluidRow <- function(...) {
 #'       "3 offset 2"
 #'     )
 #'   )
-#' ))
+#' )
+#'
+#' shinyApp(ui, server = function(input, output) { })
+#' }
 #'
 #' @rdname fixedPage
 #' @export
@@ -160,24 +184,43 @@ fixedRow <- function(...) {
 #' @seealso \code{\link{fluidRow}}, \code{\link{fixedRow}}.
 #'
 #' @examples
-#' fluidRow(
-#'   column(4,
-#'     sliderInput("obs", "Number of observations:",
-#'                 min = 1, max = 1000, value = 500)
-#'   ),
-#'   column(8,
-#'     plotOutput("distPlot")
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   fluidRow(
+#'     column(4,
+#'       sliderInput("obs", "Number of observations:",
+#'                   min = 1, max = 1000, value = 500)
+#'     ),
+#'     column(8,
+#'       plotOutput("distPlot")
+#'     )
 #'   )
 #' )
 #'
-#' fluidRow(
-#'   column(width = 4,
-#'     "4"
-#'   ),
-#'   column(width = 3, offset = 2,
-#'     "3 offset 2"
+#' server <- function(input, output) {
+#'   output$distPlot <- renderPlot({
+#'     hist(rnorm(input$obs))
+#'   })
+#' }
+#'
+#' shinyApp(ui, server)
+#'
+#'
+#'
+#' ui <- fluidPage(
+#'   fluidRow(
+#'     column(width = 4,
+#'       "4"
+#'     ),
+#'     column(width = 3, offset = 2,
+#'       "3 offset 2"
+#'     )
 #'   )
 #' )
+#' shinyApp(ui, server = function(input, output) { })
+#' }
 #' @export
 column <- function(width, ..., offset = 0) {
 
@@ -202,8 +245,14 @@ column <- function(width, ..., offset = 0) {
 #'
 #'
 #' @examples
-#' titlePanel("Hello Shiny!")
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
 #'
+#' ui <- fluidPage(
+#'   titlePanel("Hello Shiny!")
+#' )
+#' shinyApp(ui, server = function(input, output) { })
+#' }
 #' @export
 titlePanel <- function(title, windowTitle=title) {
   tagList(
@@ -226,8 +275,11 @@ titlePanel <- function(title, windowTitle=title) {
 #'   layout.
 #'
 #' @examples
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
 #' # Define UI
-#' shinyUI(fluidPage(
+#' ui <- fluidPage(
 #'
 #'   # Application title
 #'   titlePanel("Hello Shiny!"),
@@ -248,8 +300,18 @@ titlePanel <- function(title, windowTitle=title) {
 #'       plotOutput("distPlot")
 #'     )
 #'   )
-#' ))
+#' )
+#'
+#' # Server logic
+#' server <- function(input, output) {
+#'   output$distPlot <- renderPlot({
+#'     hist(rnorm(input$obs))
+#'   })
+#' }
 #'
+#' # Complete app with UI and server components
+#' shinyApp(ui, server)
+#' }
 #' @export
 sidebarLayout <- function(sidebarPanel,
                           mainPanel,
@@ -286,13 +348,18 @@ sidebarLayout <- function(sidebarPanel,
 #' @seealso \code{\link{fluidPage}}, \code{\link{flowLayout}}
 #'
 #' @examples
-#' shinyUI(fluidPage(
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
 #'   verticalLayout(
 #'     a(href="http://example.com/link1", "Link One"),
 #'     a(href="http://example.com/link2", "Link Two"),
 #'     a(href="http://example.com/link3", "Link Three")
 #'   )
-#' ))
+#' )
+#' shinyApp(ui, server = function(input, output) { })
+#' }
 #' @export
 verticalLayout <- function(..., fluid = TRUE) {
   lapply(list(...), function(row) {
@@ -319,11 +386,16 @@ verticalLayout <- function(..., fluid = TRUE) {
 #' @seealso \code{\link{verticalLayout}}
 #'
 #' @examples
-#' flowLayout(
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- flowLayout(
 #'   numericInput("rows", "How many rows?", 5),
 #'   selectInput("letter", "Which letter?", LETTERS),
 #'   sliderInput("value", "What value?", 0, 100, 50)
 #' )
+#' shinyApp(ui, server = function(input, output) { })
+#' }
 #' @export
 flowLayout <- function(..., cellArgs = list()) {
 
@@ -346,7 +418,6 @@ flowLayout <- function(..., cellArgs = list()) {
 #' suitable for wrapping inputs.
 #'
 #' @param ... Input controls or other HTML elements.
-#'
 #' @export
 inputPanel <- function(...) {
   div(class = "shiny-input-panel",
@@ -369,21 +440,33 @@ inputPanel <- function(...) {
 #'   of the layout.
 #'
 #' @examples
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' # Server code used for all examples
+#' server <- function(input, output) {
+#'   output$plot1 <- renderPlot(plot(cars))
+#'   output$plot2 <- renderPlot(plot(pressure))
+#'   output$plot3 <- renderPlot(plot(AirPassengers))
+#' }
+#'
 #' # Equal sizing
-#' splitLayout(
+#' ui <- splitLayout(
 #'   plotOutput("plot1"),
 #'   plotOutput("plot2")
 #' )
+#' shinyApp(ui, server)
 #'
 #' # Custom widths
-#' splitLayout(cellWidths = c("25%", "75%"),
+#' ui <- splitLayout(cellWidths = c("25%", "75%"),
 #'   plotOutput("plot1"),
 #'   plotOutput("plot2")
 #' )
+#' shinyApp(ui, server)
 #'
 #' # All cells at 300 pixels wide, with cell padding
 #' # and a border around everything
-#' splitLayout(
+#' ui <- splitLayout(
 #'   style = "border: 1px solid silver;",
 #'   cellWidths = 300,
 #'   cellArgs = list(style = "padding: 6px"),
@@ -391,6 +474,8 @@ inputPanel <- function(...) {
 #'   plotOutput("plot2"),
 #'   plotOutput("plot3")
 #' )
+#' shinyApp(ui, server)
+#' }
 #' @export
 splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
 
@@ -460,10 +545,7 @@ splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
 #'   not determined by the height of its contents.
 #'
 #' @examples
-#' \donttest{
 #' # Only run this example in interactive R sessions.
-#' # NOTE: This example should be run with example(fillRow, ask = FALSE) to
-#' # avoid being prompted to hit Enter during plot rendering.
 #' if (interactive()) {
 #'
 #' ui <- fillPage(fillRow(
@@ -483,7 +565,6 @@ splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
 #' shinyApp(ui, server)
 #'
 #' }
-#' }
 #' @export
 fillRow <- function(..., flex = 1, width = "100%", height = "100%") {
   flexfill(..., direction = "row", flex = flex, width = width, height = height)
diff --git a/R/bootstrap.R b/R/bootstrap.R
index 4567294..07e5e12 100644
--- a/R/bootstrap.R
+++ b/R/bootstrap.R
@@ -25,7 +25,6 @@ NULL
 #'   \code{\link{fluidPage}} function instead.
 #'
 #' @seealso \code{\link{fluidPage}}, \code{\link{fixedPage}}
-#'
 #' @export
 bootstrapPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
 
@@ -43,7 +42,7 @@ bootstrapPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
       # remainder of tags passed to the function
       list(...)
     ),
-    bootstrapDependency()
+    bootstrapLib()
   )
 }
 
@@ -61,11 +60,7 @@ bootstrapPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
 #' @inheritParams bootstrapPage
 #' @export
 bootstrapLib <- function(theme = NULL) {
-  attachDependencies(tagList(), bootstrapDependency(theme))
-}
-
-bootstrapDependency <- function(theme = NULL) {
-  htmlDependency("bootstrap", "3.3.5",
+  htmlDependency("bootstrap", "3.3.7",
     c(
       href = "shared/bootstrap",
       file = system.file("www/shared/bootstrap", package = "shiny")
@@ -157,7 +152,6 @@ basicPage <- function(...) {
 #'     div(style = "background-color: blue; width: 100%; height: 100%;")
 #'   )
 #' )
-#'
 #' @export
 fillPage <- function(..., padding = 0, title = NULL, bootstrap = TRUE,
   theme = NULL) {
@@ -200,7 +194,7 @@ collapseSizes <- function(padding) {
 #'
 #' @examples
 #' # Define UI
-#' shinyUI(pageWithSidebar(
+#' pageWithSidebar(
 #'
 #'   # Application title
 #'   headerPanel("Hello Shiny!"),
@@ -218,8 +212,7 @@ collapseSizes <- function(padding) {
 #'   mainPanel(
 #'     plotOutput("distPlot")
 #'   )
-#' ))
-#'
+#' )
 #' @export
 pageWithSidebar <- function(headerPanel,
                             sidebarPanel,
@@ -246,18 +239,24 @@ pageWithSidebar <- function(headerPanel,
 #' toggle a set of \code{\link{tabPanel}} elements.
 #'
 #' @param title The title to display in the navbar
-#' @param ... \code{\link{tabPanel}} elements to include in the page
+#' @param ... \code{\link{tabPanel}} elements to include in the page. The
+#'   \code{navbarMenu} function also accepts strings, which will be used as menu
+#'   section headers. If the string is a set of dashes like \code{"----"} a
+#'   horizontal separator will be displayed in the menu.
 #' @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}}.
+#' @param selected The \code{value} (or, if none was supplied, the \code{title})
+#'   of the tab that should be selected by default. If \code{NULL}, the first
+#'   tab will be selected.
 #' @param position Determines whether the navbar should be displayed at the top
-#'   of the page with normal scrolling behavior (\code{"static-top"}), pinned
-#'   at the top (\code{"fixed-top"}), or pinned at the bottom
+#'   of the page with normal scrolling behavior (\code{"static-top"}), pinned at
+#'   the top (\code{"fixed-top"}), or pinned at the bottom
 #'   (\code{"fixed-bottom"}). Note that using \code{"fixed-top"} or
 #'   \code{"fixed-bottom"} will cause the navbar to overlay your body content,
-#'   unless you add padding, e.g.:
-#'   \code{tags$style(type="text/css", "body {padding-top: 70px;}")}
+#'   unless you add padding, e.g.: \code{tags$style(type="text/css", "body
+#'   {padding-top: 70px;}")}
 #' @param header Tag or list of tags to display as a common header above all
 #'   tabPanels.
 #' @param footer Tag or list of tags to display as a common footer below all
@@ -289,23 +288,26 @@ pageWithSidebar <- function(headerPanel,
 #'   \code{\link{updateNavbarPage}}
 #'
 #' @examples
-#' shinyUI(navbarPage("App Title",
+#' navbarPage("App Title",
 #'   tabPanel("Plot"),
 #'   tabPanel("Summary"),
 #'   tabPanel("Table")
-#' ))
+#' )
 #'
-#' shinyUI(navbarPage("App Title",
+#' navbarPage("App Title",
 #'   tabPanel("Plot"),
 #'   navbarMenu("More",
 #'     tabPanel("Summary"),
+#'     "----",
+#'     "Section header",
 #'     tabPanel("Table")
 #'   )
-#' ))
+#' )
 #' @export
 navbarPage <- function(title,
                        ...,
                        id = NULL,
+                       selected = NULL,
                        position = c("static-top", "fixed-top", "fixed-bottom"),
                        header = NULL,
                        footer = NULL,
@@ -333,9 +335,12 @@ navbarPage <- function(title,
   if (inverse)
     navbarClass <- paste(navbarClass, "navbar-inverse")
 
+  if (!is.null(id))
+    selected <- restoreInput(id = id, default = selected)
+
   # build the tabset
   tabs <- list(...)
-  tabset <- buildTabset(tabs, "nav navbar-nav", NULL, id)
+  tabset <- buildTabset(tabs, "nav navbar-nav", NULL, id, selected)
 
   # built the container div dynamically to support optional collapsibility
   if (collapsible) {
@@ -425,7 +430,6 @@ headerPanel <- function(title, windowTitle=title) {
 #'
 #' @param ... UI elements to include inside the panel.
 #' @return The newly created panel.
-#'
 #' @export
 wellPanel <- function(...) {
   div(class="well", ...)
@@ -529,7 +533,6 @@ mainPanel <- function(..., width = 8) {
 #'       )
 #'    )
 #' )
-#'
 #' @export
 conditionalPanel <- function(condition, ...) {
   div('data-display-if'=condition, ...)
@@ -602,10 +605,8 @@ tabPanel <- function(title, ..., value = title, icon = NULL) {
 #'   tab will be selected.
 #' @param type Use "tabs" for the standard look; Use "pills" for a more plain
 #'   look where tabs are selected using a background fill color.
-#' @param position The position of the tabs relative to the content. Valid
-#'   values are "above", "below", "left", and "right" (defaults to "above").
-#'   Note that the \code{position} argument is not valid when \code{type} is
-#'   "pill".
+#' @param position This argument is deprecated; it has been discontinued in
+#'   Bootstrap 3.
 #' @return A tabset that can be passed to \code{\link{mainPanel}}
 #'
 #' @seealso \code{\link{tabPanel}}, \code{\link{updateTabsetPanel}}
@@ -625,25 +626,28 @@ tabsetPanel <- function(...,
                         id = NULL,
                         selected = NULL,
                         type = c("tabs", "pills"),
-                        position = c("above", "below", "left", "right")) {
+                        position = NULL) {
+  if (!is.null(position)) {
+    shinyDeprecated(msg = paste("tabsetPanel: argument 'position' is deprecated;",
+                                "it has been discontinued in Bootstrap 3."),
+                    version = "0.10.2.2")
+  }
+
+  if (!is.null(id))
+    selected <- restoreInput(id = id, default = selected)
 
   # build the tabset
   tabs <- list(...)
   type <- match.arg(type)
+
   tabset <- buildTabset(tabs, paste0("nav nav-", type), NULL, id, selected)
 
-  # position the nav list and content appropriately
-  position <- match.arg(position)
-  if (position %in% c("above", "left", "right")) {
-    first <- tabset$navList
-    second <- tabset$content
-  } else if (position %in% c("below")) {
-    first <- tabset$content
-    second <- tabset$navList
-  }
+  # create the content
+  first <- tabset$navList
+  second <- tabset$content
 
   # create the tab div
-  tags$div(class = paste("tabbable tabs-", position, sep=""), first, second)
+  tags$div(class = "tabbable", first, second)
 }
 
 #' Create a navigation list panel
@@ -674,7 +678,7 @@ tabsetPanel <- function(...,
 #'
 #' @seealso \code{\link{tabPanel}}, \code{\link{updateNavlistPanel}}
 #' @examples
-#' shinyUI(fluidPage(
+#' fluidPage(
 #'
 #'   titlePanel("Application Title"),
 #'
@@ -684,7 +688,7 @@ tabsetPanel <- function(...,
 #'     tabPanel("Second"),
 #'     tabPanel("Third")
 #'   )
-#' ))
+#' )
 #' @export
 navlistPanel <- function(...,
                          id = NULL,
@@ -698,6 +702,9 @@ navlistPanel <- function(...,
       tags$li(class="navbar-brand", text)
   }
 
+  if (!is.null(id))
+    selected <- restoreInput(id = id, default = selected)
+
   # build the tabset
   tabs <- list(...)
   tabset <- buildTabset(tabs,
@@ -720,117 +727,188 @@ navlistPanel <- function(...,
 }
 
 
-buildTabset <- function(tabs,
-                        ulClass,
-                        textFilter = NULL,
-                        id = NULL,
-                        selected = NULL) {
+buildTabset <- function(tabs, ulClass, textFilter = NULL,
+                        id = NULL, selected = NULL) {
 
-  # build tab nav list and tab content div
+  # This function proceeds in two phases. First, it scans over all the items
+  # to find and mark which tab should start selected. Then it actually builds
+  # the tab nav and tab content lists.
 
-  # add tab input sentinel class if we have an id
-  if (!is.null(id))
-    ulClass <- paste(ulClass, "shiny-tab-input")
-
-  tabNavList <- tags$ul(class = ulClass, id = id)
-  tabContent <- tags$div(class = "tab-content")
-  firstTab <- TRUE
-  tabsetId <- p_randomInt(1000, 10000)
-  tabId <- 1
-  for (divTag in tabs) {
-
-    # check for text; pass it to the textFilter or skip it if there is none
-    if (is.character(divTag)) {
-      if (!is.null(textFilter))
-        tabNavList <- tagAppendChild(tabNavList, textFilter(divTag))
-      next
-    }
+  # Mark an item as selected
+  markSelected <- function(x) {
+    attr(x, "selected") <- TRUE
+    x
+  }
 
-    # compute id and assign it to the div
-    thisId <- paste("tab", tabsetId, tabId, sep="-")
-    divTag$attribs$id <- thisId
-    tabId <- tabId + 1
+  # Returns TRUE if an item is selected
+  isSelected <- function(x) {
+    isTRUE(attr(x, "selected", exact = TRUE))
+  }
 
-    tabValue <- divTag$attribs$`data-value`
+  # Returns TRUE if a list of tab items contains a selected tab, FALSE
+  # otherwise.
+  containsSelected <- function(tabs) {
+    any(vapply(tabs, isSelected, logical(1)))
+  }
 
-    # function to append an optional icon to an aTag
-    appendIcon <- function(aTag, iconClass) {
-      if (!is.null(iconClass)) {
-        # for font-awesome we specify fixed-width
-        if (grepl("fa-", iconClass, fixed = TRUE))
-          iconClass <- paste(iconClass, "fa-fw")
-        aTag <- tagAppendChild(aTag, icon(name = NULL, class = iconClass))
+  # Take a pass over all tabs, and mark the selected tab.
+  foundSelectedItem <- FALSE
+  findAndMarkSelected <- function(tabs, selected) {
+    lapply(tabs, function(divTag) {
+      if (foundSelectedItem) {
+        # If we already found the selected tab, no need to keep looking
+
+      } else if (is.character(divTag)) {
+        # Strings don't represent selectable items
+
+      } else if (inherits(divTag, "shiny.navbarmenu")) {
+        # Navbar menu
+        divTag$tabs <- findAndMarkSelected(divTag$tabs, selected)
+
+      } else {
+        # Regular tab item
+        if (is.null(selected)) {
+          # If selected tab isn't specified, mark first available item
+          # as selected.
+          foundSelectedItem <<- TRUE
+          divTag <- markSelected(divTag)
+
+        } else {
+          # If selected tab is specified, check for a match
+          tabValue <- divTag$attribs$`data-value` %OR% divTag$attribs$title
+          if (identical(selected, tabValue)) {
+            foundSelectedItem <<- TRUE
+            divTag <- markSelected(divTag)
+          }
+        }
       }
-      aTag
-    }
-
-    # check for a navbarMenu and handle appropriately
-    if (inherits(divTag, "shiny.navbarmenu")) {
-
-      # create the a tag
-      aTag <- tags$a(href="#",
-                     class="dropdown-toggle",
-                     `data-toggle`="dropdown")
-
-      # add optional icon
-      aTag <- appendIcon(aTag, divTag$iconClass)
-
-      # add the title and caret
-      aTag <- tagAppendChild(aTag, divTag$title)
-      aTag <- tagAppendChild(aTag, tags$b(class="caret"))
 
-      # build the dropdown list element
-      liTag <- tags$li(class = "dropdown", aTag)
-
-      # build the child tabset
-      tabset <- buildTabset(divTag$tabs, "dropdown-menu")
-      liTag <- tagAppendChild(liTag, tabset$navList)
-
-      # don't add a standard tab content div, rather add the list of tab
-      # content divs that are contained within the tabset
-      divTag <- NULL
-      tabContent <- tagAppendChildren(tabContent,
-                                      list = tabset$content$children)
-    }
-    # else it's a standard navbar item
-    else {
-      # create the a tag
-      aTag <- tags$a(href=paste("#", thisId, sep=""),
-                     `data-toggle` = "tab",
-                     `data-value` = tabValue)
-
-      # append optional icon
-      aTag <- appendIcon(aTag, divTag$attribs$`data-icon-class`)
+      return(divTag)
+    })
+  }
 
-      # add the title
-      aTag <- tagAppendChild(aTag, divTag$attribs$title)
 
-      # create the li tag
-      liTag <- tags$li(aTag)
+  # Append an optional icon to an aTag
+  appendIcon <- function(aTag, iconClass) {
+    if (!is.null(iconClass)) {
+      # for font-awesome we specify fixed-width
+      if (grepl("fa-", iconClass, fixed = TRUE))
+        iconClass <- paste(iconClass, "fa-fw")
+      aTag <- tagAppendChild(aTag, icon(name = NULL, class = iconClass))
     }
+    aTag
+  }
 
-    if (is.null(tabValue)) {
-      tabValue <- divTag$attribs$title
+  # Build the tabset
+  build <- function(tabs, ulClass, textFilter = NULL, id = NULL) {
+    # add tab input sentinel class if we have an id
+    if (!is.null(id))
+      ulClass <- paste(ulClass, "shiny-tab-input")
+
+    if (anyNamed(tabs)) {
+      nms <- names(tabs)
+      nms <- nms[nzchar(nms)]
+      stop("Tabs should all be unnamed arguments, but some are named: ",
+           paste(nms, collapse = ", "))
     }
 
-    # If appropriate, make this the selected tab (don't ever do initial
-    # selection of tabs that are within a navbarMenu)
-    if ((ulClass != "dropdown-menu") &&
-       ((firstTab && is.null(selected)) ||
-        (!is.null(selected) && identical(selected, tabValue)))) {
-      liTag$attribs$class <- "active"
-      divTag$attribs$class <- "tab-pane active"
-      firstTab = FALSE
+    tabNavList <- tags$ul(class = ulClass, id = id)
+    tabContent <- tags$div(class = "tab-content")
+    tabsetId <- p_randomInt(1000, 10000)
+    tabId <- 1
+
+    buildItem <- function(divTag) {
+      # check for text; pass it to the textFilter or skip it if there is none
+      if (is.character(divTag)) {
+        if (!is.null(textFilter)) {
+          tabNavList <<- tagAppendChild(tabNavList, textFilter(divTag))
+        }
+
+      } else if (inherits(divTag, "shiny.navbarmenu")) {
+
+        # create the a tag
+        aTag <- tags$a(href="#",
+                       class="dropdown-toggle",
+                       `data-toggle`="dropdown")
+
+        # add optional icon
+        aTag <- appendIcon(aTag, divTag$iconClass)
+
+        # add the title and caret
+        aTag <- tagAppendChild(aTag, divTag$title)
+        aTag <- tagAppendChild(aTag, tags$b(class="caret"))
+
+        # build the dropdown list element
+        liTag <- tags$li(class = "dropdown", aTag)
+
+        # text filter for separators
+        textFilter <- function(text) {
+          if (grepl("^\\-+$", text))
+            tags$li(class="divider")
+          else
+            tags$li(class="dropdown-header", text)
+        }
+
+        # build the child tabset
+        tabset <- build(divTag$tabs, "dropdown-menu", textFilter)
+        liTag <- tagAppendChild(liTag, tabset$navList)
+
+        # If this navbar menu contains a selected item, mark it as active
+        if (containsSelected(divTag$tabs)) {
+          liTag$attribs$class <- paste(liTag$attribs$class, "active")
+        }
+
+        tabNavList <<- tagAppendChild(tabNavList, liTag)
+        # don't add a standard tab content div, rather add the list of tab
+        # content divs that are contained within the tabset
+        tabContent <<- tagAppendChildren(tabContent,
+                                        list = tabset$content$children)
+
+      } else {
+        # Standard navbar item
+        # compute id and assign it to the div
+        thisId <- paste("tab", tabsetId, tabId, sep="-")
+        divTag$attribs$id <- thisId
+        tabId <<- tabId + 1
+
+        tabValue <- divTag$attribs$`data-value`
+
+        # create the a tag
+        aTag <- tags$a(href=paste("#", thisId, sep=""),
+                       `data-toggle` = "tab",
+                       `data-value` = tabValue)
+
+        # append optional icon
+        aTag <- appendIcon(aTag, divTag$attribs$`data-icon-class`)
+
+        # add the title
+        aTag <- tagAppendChild(aTag, divTag$attribs$title)
+
+        # create the li tag
+        liTag <- tags$li(aTag)
+
+        # If selected, set appropriate classes on li tag and div tag.
+        if (isSelected(divTag)) {
+          liTag$attribs$class <- "active"
+          divTag$attribs$class <- "tab-pane active"
+        }
+
+        divTag$attribs$title <- NULL
+
+        # append the elements to our lists
+        tabNavList <<- tagAppendChild(tabNavList, liTag)
+        tabContent <<- tagAppendChild(tabContent, divTag)
+      }
     }
 
-    divTag$attribs$title <- NULL
-
-    # append the elements to our lists
-    tabNavList <- tagAppendChild(tabNavList, liTag)
-    tabContent <- tagAppendChild(tabContent, divTag)
+    lapply(tabs, buildItem)
+    list(navList = tabNavList, content = tabContent)
   }
 
-  list(navList = tabNavList, content = tabContent)
+
+  # Finally, actually invoke the functions to do the processing.
+  tabs <- findAndMarkSelected(tabs, selected)
+  build(tabs, ulClass, textFilter, id)
 }
 
 
@@ -1417,12 +1495,11 @@ downloadLink <- function(outputId, label="Download", class=NULL) {
 #' # add an icon to a submit button
 #' submitButton("Update View", icon = icon("refresh"))
 #'
-#' shinyUI(navbarPage("App Title",
+#' navbarPage("App Title",
 #'   tabPanel("Plot", icon = icon("bar-chart-o")),
 #'   tabPanel("Summary", icon = icon("list-alt")),
 #'   tabPanel("Table", icon = icon("table"))
-#' ))
-#'
+#' )
 #' @export
 icon <- function(name, class = NULL, lib = "font-awesome") {
   prefixes <- list(
@@ -1450,7 +1527,7 @@ icon <- function(name, class = NULL, lib = "font-awesome") {
   # font-awesome needs an additional dependency (glyphicon is in bootstrap)
   if (lib == "font-awesome") {
     htmlDependencies(iconTag) <- htmlDependency(
-      "font-awesome", "4.5.0", c(href="shared/font-awesome"),
+      "font-awesome", "4.6.3", c(href="shared/font-awesome"),
       stylesheet = "css/font-awesome.min.css"
     )
   }
diff --git a/R/conditions.R b/R/conditions.R
index 0fbc640..93b4922 100644
--- a/R/conditions.R
+++ b/R/conditions.R
@@ -76,7 +76,7 @@ getCallNames <- function(calls) {
 }
 
 getLocs <- function(calls) {
-  sapply(calls, function(call) {
+  vapply(calls, function(call) {
     srcref <- attr(call, "srcref", exact = TRUE)
     if (!is.null(srcref)) {
       srcfile <- attr(srcref, "srcfile", exact = TRUE)
@@ -86,7 +86,7 @@ getLocs <- function(calls) {
       }
     }
     return("")
-  })
+  }, character(1))
 }
 
 #' @details \code{captureStackTraces} runs the given \code{expr} and if any
@@ -131,9 +131,10 @@ withLogErrors <- function(expr,
     captureStackTraces(expr),
     error = function(cond) {
       # Don't print shiny.silent.error (i.e. validation errors)
-      if (inherits(cond, "shiny.silent.error"))
-        return()
-      printError(cond, full = full, offset = offset)
+      if (inherits(cond, "shiny.silent.error")) return()
+      if (isTRUE(getOption("show.error.messages"))) {
+        printError(cond, full = full, offset = offset)
+      }
     }
   )
 }
diff --git a/R/diagnose.R b/R/diagnose.R
new file mode 100644
index 0000000..40c8ed3
--- /dev/null
+++ b/R/diagnose.R
@@ -0,0 +1,157 @@
+# Analyze an R file for possible extra or missing commas. Returns FALSE if any
+# problems detected, TRUE otherwise.
+diagnoseCode <- function(path = NULL, text = NULL) {
+  if (!xor(is.null(path), is.null(text))) {
+    stop("Must specify `path` or `text`, but not both.")
+  }
+
+  if (!is.null(path)) {
+    tokens <- sourcetools::tokenize_file(path)
+  } else {
+    tokens <- sourcetools::tokenize_string(text)
+  }
+
+  find_scopes <- function(tokens) {
+    # Strip whitespace and comments
+    tokens <- tokens[!(tokens$type %in% c("whitespace", "comment")),]
+
+    # Replace various types of things with "value"
+    tokens$type[tokens$type %in% c("string", "number", "symbol", "keyword")] <- "value"
+
+    # Record types for close and open brace/bracket/parens, and commas
+    brace_idx <- tokens$value %in% c("(", ")", "{", "}", "[", "]", ",")
+    tokens$type[brace_idx] <- tokens$value[brace_idx]
+
+    # Stack-related function for recording scope. Starting scope is "{"
+    stack <- "{"
+    push <- function(x) {
+      stack <<- c(stack, x)
+    }
+    pop <- function() {
+      if (length(stack) == 1) {
+        # Stack underflow, but we need to keep going
+        return(NA_character_)
+      }
+      res <- stack[length(stack)]
+      stack <<- stack[-length(stack)]
+      res
+    }
+    peek <- function() {
+      stack[length(stack)]
+    }
+
+    # First, establish a scope for each token. For opening and closing
+    # braces/brackets/parens, the scope at that location is the *surrounding*
+    # scope, not the new scope created by the brace/bracket/paren.
+    for (i in seq_len(nrow(tokens))) {
+      value <- tokens$value[i]
+
+      tokens$scope[i] <- peek()
+      if (value %in% c("{", "(", "[")) {
+        push(value)
+
+      } else if (value == "}") {
+        if (!identical(pop(), "{"))
+          tokens$err[i] <- "unmatched_brace"
+        # For closing brace/paren/bracket, get the scope after popping
+        tokens$scope[i] <- peek()
+
+      } else if (value == ")") {
+        if (!identical(pop(), "("))
+          tokens$err[i] <- "unmatched_paren"
+        tokens$scope[i] <- peek()
+
+      } else if (value == "]") {
+        if (!identical(pop(), "["))
+          tokens$err[i] <- "unmatched_bracket"
+        tokens$scope[i] <- peek()
+      }
+    }
+
+    tokens
+  }
+
+  check_commas <- function(tokens) {
+    # Find extra and missing commas
+    tokens$err <- mapply(
+      tokens$type,
+      c("", tokens$type[-length(tokens$type)]),
+      c(tokens$type[-1],  ""),
+      tokens$scope,
+      tokens$err,
+      SIMPLIFY = FALSE,
+      FUN = function(type, prevType, nextType, scope, err) {
+        # If an error was already found, just return it. This could have
+        # happened in the brace/paren/bracket matching phase.
+        if (!is.na(err)) {
+          return(err)
+        }
+        if (scope == "(") {
+          if (type == "," &&
+              (prevType == "(" || prevType == "," || nextType == ")"))
+          {
+            return("extra_comma")
+          }
+
+          if ((prevType == ")" && type == "value") ||
+              (prevType == "value" && type == "value")) {
+            return("missing_comma")
+          }
+        }
+
+        NA_character_
+      }
+    )
+
+    tokens
+  }
+
+
+  tokens$err <- NA_character_
+  tokens <- find_scopes(tokens)
+  tokens <- check_commas(tokens)
+
+  # No errors found
+  if (all(is.na(tokens$err))) {
+    return(TRUE)
+  }
+
+  # If we got here, errors were found; print messages.
+  if (!is.null(path)) {
+    lines <- readLines(path)
+  } else {
+    lines <- strsplit(text, "\n")[[1]]
+  }
+
+  # Print out the line of code with the error, and point to the column with
+  # the error.
+  show_code_error <- function(msg, lines, row, col) {
+    message(paste0(
+      msg, "\n",
+      row, ":", lines[row], "\n",
+      paste0(rep.int(" ", nchar(as.character(row)) + 1), collapse = ""),
+      gsub(perl = TRUE, "[^\\s]", " ", substr(lines[row], 1, col-1)), "^"
+    ))
+  }
+
+  err_idx <- which(!is.na(tokens$err))
+  msg <- ""
+  for (i in err_idx) {
+    row <- tokens$row[i]
+    col <- tokens$column[i]
+    err <- tokens$err[i]
+
+    if (err == "missing_comma") {
+      show_code_error("Possible missing comma at:", lines, row, col)
+    } else if (err == "extra_comma") {
+      show_code_error("Possible extra comma at:", lines, row, col)
+    } else if (err == "unmatched_brace") {
+      show_code_error("Possible unmatched '}' at:", lines, row, col)
+    } else if (err == "unmatched_paren") {
+      show_code_error("Possible unmatched ')' at:", lines, row, col)
+    } else if (err == "unmatched_bracket") {
+      show_code_error("Possible unmatched ']' at:", lines, row, col)
+    }
+  }
+  return(FALSE)
+}
diff --git a/R/fileupload.R b/R/fileupload.R
index 4d47215..8b30c5e 100644
--- a/R/fileupload.R
+++ b/R/fileupload.R
@@ -94,7 +94,7 @@ FileUploadContext <- R6Class(
     },
     createUploadOperation = function(fileInfos) {
       while (TRUE) {
-        id <- paste(as.raw(p_runif(12, min=0, max=0xFF)), collapse='')
+        id <- createUniqueId(12)
         private$ids <- c(private$ids, id)
         dir <- file.path(private$basedir, id)
         if (!dir.create(dir))
diff --git a/R/graph.R b/R/graph.R
index 480c3c3..d3d0a4c 100644
--- a/R/graph.R
+++ b/R/graph.R
@@ -41,12 +41,14 @@ writeReactLog <- function(file=stdout(), sessionToken = NULL) {
 #' enabled, it's possible for any user of your app to see at least some
 #' of the source code of your reactive expressions and observers.
 #'
+#' @param time A boolean that specifies whether or not to display the
+#' time that each reactive.
 #' @export
-showReactLog <- function() {
-  utils::browseURL(renderReactLog())
+showReactLog <- function(time = TRUE) {
+  utils::browseURL(renderReactLog(time = as.logical(time)))
 }
 
-renderReactLog <- function(sessionToken = NULL) {
+renderReactLog <- function(sessionToken = NULL, time = TRUE) {
   templateFile <- system.file('www/reactive-graph.html', package='shiny')
   html <- paste(readLines(templateFile, warn=FALSE), collapse='\r\n')
   tc <- textConnection(NULL, 'w')
@@ -55,6 +57,7 @@ renderReactLog <- function(sessionToken = NULL) {
   cat('\n', file=tc)
   flush(tc)
   html <- sub('__DATA__', paste(textConnectionValue(tc), collapse='\r\n'), html, fixed=TRUE)
+  html <- sub('__TIME__', paste0('"', time, '"'), html, fixed=TRUE)
   file <- tempfile(fileext = '.html')
   writeLines(html, file)
   return(file)
diff --git a/R/html-deps.R b/R/html-deps.R
index d48958f..7dfab12 100644
--- a/R/html-deps.R
+++ b/R/html-deps.R
@@ -6,7 +6,7 @@
 #' URL.
 #'
 #' @param dependency A single HTML dependency object, created using
-#'   \code{\link{htmlDependency}}. If the \code{src} value is named, then
+#'   \code{\link[htmltools]{htmlDependency}}. If the \code{src} value is named, then
 #'   \code{href} and/or \code{file} names must be present.
 #'
 #' @return A single HTML dependency object that has an \code{href}-named element
@@ -27,3 +27,21 @@ createWebDependency <- function(dependency) {
 
   return(dependency)
 }
+
+
+# Given a Shiny tag object, process singletons and dependencies. Returns a list
+# with rendered HTML and dependency objects.
+processDeps <- function(tags, session) {
+  ui <- takeSingletons(tags, session$singletons, desingleton=FALSE)$ui
+  ui <- surroundSingletons(ui)
+  dependencies <- lapply(
+    resolveDependencies(findDependencies(ui)),
+    createWebDependency
+  )
+  names(dependencies) <- NULL
+
+  list(
+    html = doRenderTags(ui),
+    deps = dependencies
+  )
+}
diff --git a/R/imageutils.R b/R/imageutils.R
index 8ed401b..376809d 100644
--- a/R/imageutils.R
+++ b/R/imageutils.R
@@ -25,7 +25,6 @@
 #'   R; it won't change the actual ppi of the browser.
 #' @param ... Arguments to be passed through to \code{\link[grDevices]{png}}.
 #'   These can be used to set the width, height, background color, etc.
-#'
 #' @export
 plotPNG <- function(func, filename=tempfile(fileext='.png'),
                     width=400, height=400, res=72, ...) {
diff --git a/R/input-action.R b/R/input-action.R
index 6ec896d..3ade526 100644
--- a/R/input-action.R
+++ b/R/input-action.R
@@ -11,30 +11,43 @@
 #'
 #' @family input elements
 #' @examples
-#' \dontrun{
-#' # In server.R
-#' output$distPlot <- renderPlot({
-#'   # Take a dependency on input$goButton
-#'   input$goButton
-#'
-#'   # Use isolate() to avoid dependency on input$obs
-#'   dist <- isolate(rnorm(input$obs))
-#'   hist(dist)
-#' })
-#'
-#' # In ui.R
-#' actionButton("goButton", "Go!")
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   sliderInput("obs", "Number of observations", 0, 1000, 500),
+#'   actionButton("goButton", "Go!"),
+#'   plotOutput("distPlot")
+#' )
+#'
+#' server <- function(input, output) {
+#'   output$distPlot <- renderPlot({
+#'     # Take a dependency on input$goButton. This will run once initially,
+#'     # because the value changes from NULL to 0.
+#'     input$goButton
+#'
+#'     # Use isolate() to avoid dependency on input$obs
+#'     dist <- isolate(rnorm(input$obs))
+#'     hist(dist)
+#'   })
 #' }
 #'
-#' @seealso \code{\link{observeEvent}} and \code{\link{eventReactive}}
+#' shinyApp(ui, server)
 #'
+#' }
+#'
+#' @seealso \code{\link{observeEvent}} and \code{\link{eventReactive}}
 #' @export
 actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
+
+  value <- restoreInput(id = inputId, default = NULL)
+
   tags$button(id=inputId,
     style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
     type="button",
     class="btn btn-default action-button",
-    list(icon, label),
+    `data-val` = value,
+    list(validateIcon(icon), label),
     ...
   )
 }
@@ -42,10 +55,32 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
 #' @rdname actionButton
 #' @export
 actionLink <- function(inputId, label, icon = NULL, ...) {
+  value <- restoreInput(id = inputId, default = NULL)
+
   tags$a(id=inputId,
     href="#",
     class="action-button",
-    list(icon, label),
+    `data-val` = value,
+    list(validateIcon(icon), label),
     ...
   )
 }
+
+
+# Check that the icon parameter is valid:
+# 1) Check  if the user wants to actually add an icon:
+#    -- if icon=NULL, it means leave the icon unchanged
+#    -- if icon=character(0), it means don't add an icon or, more usefully,
+#       remove the previous icon
+# 2) If so, check that the icon has the right format (this does not check whether
+# it is a *real* icon - currently that would require a massive cross reference
+# with the "font-awesome" and the "glyphicon" libraries)
+validateIcon <- function(icon) {
+  if (is.null(icon) || identical(icon, character(0))) {
+    return(icon)
+  } else if (inherits(icon, "shiny.tag") && icon$name == "i") {
+    return(icon)
+  } else {
+    stop("Invalid icon. Use Shiny's 'icon()' function to generate a valid icon")
+  }
+}
diff --git a/R/input-checkbox.R b/R/input-checkbox.R
index 988573e..30f638d 100644
--- a/R/input-checkbox.R
+++ b/R/input-checkbox.R
@@ -10,9 +10,23 @@
 #' @seealso \code{\link{checkboxGroupInput}}, \code{\link{updateCheckboxInput}}
 #'
 #' @examples
-#' checkboxInput("outliers", "Show outliers", FALSE)
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   checkboxInput("somevalue", "Some value", FALSE),
+#'   verbatimTextOutput("value")
+#' )
+#' server <- function(input, output) {
+#'   output$value <- renderText({ input$somevalue })
+#' }
+#' shinyApp(ui, server)
+#' }
 #' @export
 checkboxInput <- function(inputId, label, value = FALSE, width = NULL) {
+
+  value <- restoreInput(id = inputId, default = value)
+
   inputTag <- tags$input(id = inputId, type="checkbox")
   if (!is.null(value) && value)
     inputTag$attribs$checked <- "checked"
diff --git a/R/input-checkboxgroup.R b/R/input-checkboxgroup.R
index 8e78bb2..2466472 100644
--- a/R/input-checkboxgroup.R
+++ b/R/input-checkboxgroup.R
@@ -15,15 +15,31 @@
 #' @seealso \code{\link{checkboxInput}}, \code{\link{updateCheckboxGroupInput}}
 #'
 #' @examples
-#' checkboxGroupInput("variable", "Variable:",
-#'                    c("Cylinders" = "cyl",
-#'                      "Transmission" = "am",
-#'                      "Gears" = "gear"))
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
 #'
+#' ui <- fluidPage(
+#'   checkboxGroupInput("variable", "Variables to show:",
+#'                      c("Cylinders" = "cyl",
+#'                        "Transmission" = "am",
+#'                        "Gears" = "gear")),
+#'   tableOutput("data")
+#' )
+#'
+#' server <- function(input, output) {
+#'   output$data <- renderTable({
+#'     mtcars[, c("mpg", input$variable), drop = FALSE]
+#'   }, rownames = TRUE)
+#' }
+#'
+#' shinyApp(ui, server)
+#' }
 #' @export
 checkboxGroupInput <- function(inputId, label, choices, selected = NULL,
   inline = FALSE, width = NULL) {
 
+  selected <- restoreInput(id = inputId, default = selected)
+
   # resolve names
   choices <- choicesWithNames(choices)
   if (!is.null(selected))
diff --git a/R/input-date.R b/R/input-date.R
index ece5d51..9a5c501 100644
--- a/R/input-date.R
+++ b/R/input-date.R
@@ -21,48 +21,58 @@
 #'
 #' @inheritParams textInput
 #' @param value The starting date. Either a Date object, or a string in
-#'   \code{yyyy-mm-dd} format. If NULL (the default), will use the current
-#'   date in the client's time zone.
+#'   \code{yyyy-mm-dd} format. If NULL (the default), will use the current date
+#'   in the client's time zone.
 #' @param min The minimum allowed date. Either a Date object, or a string in
 #'   \code{yyyy-mm-dd} format.
 #' @param max The maximum allowed date. Either a Date object, or a string in
 #'   \code{yyyy-mm-dd} format.
 #' @param format The format of the date to display in the browser. Defaults to
 #'   \code{"yyyy-mm-dd"}.
-#' @param startview The date range shown when the input object is first
-#'   clicked. Can be "month" (the default), "year", or "decade".
+#' @param startview The date range shown when the input object is first clicked.
+#'   Can be "month" (the default), "year", or "decade".
 #' @param weekstart Which day is the start of the week. Should be an integer
 #'   from 0 (Sunday) to 6 (Saturday).
 #' @param language The language used for month and day names. Default is "en".
-#'   Other valid values include "bg", "ca", "cs", "da", "de", "el", "es", "fi",
-#'   "fr", "he", "hr", "hu", "id", "is", "it", "ja", "kr", "lt", "lv", "ms",
-#'   "nb", "nl", "pl", "pt", "pt-BR", "ro", "rs", "rs-latin", "ru", "sk", "sl",
-#'   "sv", "sw", "th", "tr", "uk", "zh-CN", and "zh-TW".
+#'   Other valid values include "ar", "az", "bg", "bs", "ca", "cs", "cy", "da",
+#'   "de", "el", "en-AU", "en-GB", "eo", "es", "et", "eu", "fa", "fi", "fo",
+#'   "fr-CH", "fr", "gl", "he", "hr", "hu", "hy", "id", "is", "it-CH", "it",
+#'   "ja", "ka", "kh", "kk", "ko", "kr", "lt", "lv", "me", "mk", "mn", "ms",
+#'   "nb", "nl-BE", "nl", "no", "pl", "pt-BR", "pt", "ro", "rs-latin", "rs",
+#'   "ru", "sk", "sl", "sq", "sr-latin", "sr", "sv", "sw", "th", "tr", "uk",
+#'   "vi", "zh-CN", and "zh-TW".
 #'
 #' @family input elements
 #' @seealso \code{\link{dateRangeInput}}, \code{\link{updateDateInput}}
 #'
 #' @examples
-#' dateInput("date", "Date:", value = "2012-02-29")
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
 #'
-#' # Default value is the date in client's time zone
-#' dateInput("date", "Date:")
+#' ui <- fluidPage(
+#'   dateInput("date1", "Date:", value = "2012-02-29"),
 #'
-#' # value is always yyyy-mm-dd, even if the display format is different
-#' dateInput("date", "Date:", value = "2012-02-29", format = "mm/dd/yy")
+#'   # Default value is the date in client's time zone
+#'   dateInput("date2", "Date:"),
 #'
-#' # Pass in a Date object
-#' dateInput("date", "Date:", value = Sys.Date()-10)
+#'   # value is always yyyy-mm-dd, even if the display format is different
+#'   dateInput("date3", "Date:", value = "2012-02-29", format = "mm/dd/yy"),
 #'
-#' # Use different language and different first day of week
-#' dateInput("date", "Date:",
-#'           language = "de",
-#'           weekstart = 1)
+#'   # Pass in a Date object
+#'   dateInput("date4", "Date:", value = Sys.Date()-10),
 #'
-#' # Start with decade view instead of default month view
-#' dateInput("date", "Date:",
-#'           startview = "decade")
+#'   # Use different language and different first day of week
+#'   dateInput("date5", "Date:",
+#'           language = "ru",
+#'           weekstart = 1),
 #'
+#'   # Start with decade view instead of default month view
+#'   dateInput("date6", "Date:",
+#'             startview = "decade")
+#' )
+#'
+#' shinyApp(ui, server = function(input, output) { })
+#' }
 #' @export
 dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
   format = "yyyy-mm-dd", startview = "month", weekstart = 0, language = "en",
@@ -74,29 +84,36 @@ dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
   if (inherits(min,   "Date"))  min   <- format(min,   "%Y-%m-%d")
   if (inherits(max,   "Date"))  max   <- format(max,   "%Y-%m-%d")
 
-  attachDependencies(
-    tags$div(id = inputId,
-      class = "shiny-date-input form-group shiny-input-container",
-      style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
+  value <- restoreInput(id = inputId, default = value)
+
+  tags$div(id = inputId,
+    class = "shiny-date-input form-group shiny-input-container",
+    style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
 
-      controlLabel(inputId, label),
-      tags$input(type = "text",
-                 # datepicker class necessary for dropdown to display correctly
-                 class = "form-control datepicker",
-                 `data-date-language` = language,
-                 `data-date-weekstart` = weekstart,
-                 `data-date-format` = format,
-                 `data-date-start-view` = startview,
-                 `data-min-date` = min,
-                 `data-max-date` = max,
-                 `data-initial-date` = value
-      )
+    controlLabel(inputId, label),
+    tags$input(type = "text",
+               class = "form-control",
+               `data-date-language` = language,
+               `data-date-week-start` = weekstart,
+               `data-date-format` = format,
+               `data-date-start-view` = startview,
+               `data-min-date` = min,
+               `data-max-date` = max,
+               `data-initial-date` = value
     ),
     datePickerDependency
   )
 }
 
 datePickerDependency <- htmlDependency(
-  "bootstrap-datepicker", "1.0.2", c(href = "shared/datepicker"),
+  "bootstrap-datepicker", "1.6.4", c(href = "shared/datepicker"),
   script = "js/bootstrap-datepicker.min.js",
-  stylesheet = "css/datepicker.css")
+  stylesheet = "css/bootstrap-datepicker3.min.css",
+  # Need to enable noConflict mode. See #1346.
+  head = "<script>
+(function() {
+  var datepicker = $.fn.datepicker.noConflict();
+  $.fn.bsDatepicker = datepicker;
+})();
+</script>"
+)
diff --git a/R/input-daterange.R b/R/input-daterange.R
index bc7baf6..0b9a7c5 100644
--- a/R/input-daterange.R
+++ b/R/input-daterange.R
@@ -32,37 +32,44 @@
 #' @seealso \code{\link{dateInput}}, \code{\link{updateDateRangeInput}}
 #'
 #' @examples
-#' dateRangeInput("daterange", "Date range:",
-#'                start = "2001-01-01",
-#'                end   = "2010-12-31")
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
 #'
-#' # Default start and end is the current date in the client's time zone
-#' dateRangeInput("daterange", "Date range:")
+#' ui <- fluidPage(
+#'   dateRangeInput("daterange1", "Date range:",
+#'                  start = "2001-01-01",
+#'                  end   = "2010-12-31"),
 #'
-#' # start and end are always specified in yyyy-mm-dd, even if the display
-#' # format is different
-#' dateRangeInput("daterange", "Date range:",
-#'                start  = "2001-01-01",
-#'                end    = "2010-12-31",
-#'                min    = "2001-01-01",
-#'                max    = "2012-12-21",
-#'                format = "mm/dd/yy",
-#'                separator = " - ")
+#'   # Default start and end is the current date in the client's time zone
+#'   dateRangeInput("daterange2", "Date range:"),
 #'
-#' # Pass in Date objects
-#' dateRangeInput("daterange", "Date range:",
-#'                start = Sys.Date()-10,
-#'                end = Sys.Date()+10)
+#'   # start and end are always specified in yyyy-mm-dd, even if the display
+#'   # format is different
+#'   dateRangeInput("daterange3", "Date range:",
+#'                  start  = "2001-01-01",
+#'                  end    = "2010-12-31",
+#'                  min    = "2001-01-01",
+#'                  max    = "2012-12-21",
+#'                  format = "mm/dd/yy",
+#'                  separator = " - "),
 #'
-#' # Use different language and different first day of week
-#' dateRangeInput("daterange", "Date range:",
-#'                language = "de",
-#'                weekstart = 1)
+#'   # Pass in Date objects
+#'   dateRangeInput("daterange4", "Date range:",
+#'                  start = Sys.Date()-10,
+#'                  end = Sys.Date()+10),
 #'
-#' # Start with decade view instead of default month view
-#' dateRangeInput("daterange", "Date range:",
-#'                startview = "decade")
+#'   # Use different language and different first day of week
+#'   dateRangeInput("daterange5", "Date range:",
+#'                  language = "de",
+#'                  weekstart = 1),
 #'
+#'   # Start with decade view instead of default month view
+#'   dateRangeInput("daterange6", "Date range:",
+#'                  startview = "decade")
+#' )
+#'
+#' shinyApp(ui, server = function(input, output) { })
+#' }
 #' @export
 dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
     min = NULL, max = NULL, format = "yyyy-mm-dd", startview = "month",
@@ -75,6 +82,10 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
   if (inherits(min,   "Date"))  min   <- format(min,   "%Y-%m-%d")
   if (inherits(max,   "Date"))  max   <- format(max,   "%Y-%m-%d")
 
+  restored <- restoreInput(id = inputId, default = list(start, end))
+  start <- restored[[1]]
+  end <- restored[[2]]
+
   attachDependencies(
     div(id = inputId,
       class = "shiny-date-range-input form-group shiny-input-container",
diff --git a/R/input-file.R b/R/input-file.R
index d11c1c8..0ff51a6 100644
--- a/R/input-file.R
+++ b/R/input-file.R
@@ -28,20 +28,92 @@
 #' @param accept A character vector of MIME types; gives the browser a hint of
 #'   what kind of files the server is expecting.
 #'
+#' @examples
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   sidebarLayout(
+#'     sidebarPanel(
+#'       fileInput("file1", "Choose CSV File",
+#'         accept = c(
+#'           "text/csv",
+#'           "text/comma-separated-values,text/plain",
+#'           ".csv")
+#'         ),
+#'       tags$hr(),
+#'       checkboxInput("header", "Header", TRUE)
+#'     ),
+#'     mainPanel(
+#'       tableOutput("contents")
+#'     )
+#'   )
+#' )
+#'
+#' server <- function(input, output) {
+#'   output$contents <- renderTable({
+#'     # input$file1 will be NULL initially. After the user selects
+#'     # and uploads a file, it will be a data frame with 'name',
+#'     # 'size', 'type', and 'datapath' columns. The 'datapath'
+#'     # column will contain the local filenames where the data can
+#'     # be found.
+#'     inFile <- input$file1
+#'
+#'     if (is.null(inFile))
+#'       return(NULL)
+#'
+#'     read.csv(inFile$datapath, header = input$header)
+#'   })
+#' }
+#'
+#' shinyApp(ui, server)
+#' }
 #' @export
 fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
   width = NULL) {
 
-  inputTag <- tags$input(id = inputId, name = inputId, type = "file")
+  restoredValue <- restoreInput(id = inputId, default = NULL)
+
+  # Catch potential edge case - ensure that it's either NULL or a data frame.
+  if (!is.null(restoredValue) && !is.data.frame(restoredValue)) {
+    warning("Restored value for ", inputId, " has incorrect format.")
+    restoredValue <- NULL
+  }
+
+  if (!is.null(restoredValue)) {
+    restoredValue <- toJSON(restoredValue, strict_atomic = FALSE)
+  }
+
+  inputTag <- tags$input(
+    id = inputId,
+    name = inputId,
+    type = "file",
+    style = "display: none;",
+    `data-restore` = restoredValue
+  )
+
   if (multiple)
     inputTag$attribs$multiple <- "multiple"
   if (length(accept) > 0)
     inputTag$attribs$accept <- paste(accept, collapse=',')
 
+
   div(class = "form-group shiny-input-container",
     style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
     label %AND% tags$label(label),
-    inputTag,
+
+    div(class = "input-group",
+      tags$label(class = "input-group-btn",
+        span(class = "btn btn-default btn-file",
+          "Browse...",
+          inputTag
+        )
+      ),
+      tags$input(type = "text", class = "form-control",
+        placeholder = "No file selected", readonly = "readonly"
+      )
+    ),
+
     tags$div(
       id=paste(inputId, "_progress", sep=""),
       class="progress progress-striped active shiny-file-input-progress",
diff --git a/R/input-numeric.R b/R/input-numeric.R
index a33ca40..cca3757 100644
--- a/R/input-numeric.R
+++ b/R/input-numeric.R
@@ -1,4 +1,3 @@
-
 #' Create a numeric input control
 #'
 #' Create an input control for entry of numeric values
@@ -13,12 +12,24 @@
 #' @seealso \code{\link{updateNumericInput}}
 #'
 #' @examples
-#' numericInput("obs", "Observations:", 10,
-#'              min = 1, max = 100)
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   numericInput("obs", "Observations:", 10, min = 1, max = 100),
+#'   verbatimTextOutput("value")
+#' )
+#' server <- function(input, output) {
+#'   output$value <- renderText({ input$obs })
+#' }
+#' shinyApp(ui, server)
+#' }
 #' @export
 numericInput <- function(inputId, label, value, min = NA, max = NA, step = NA,
   width = NULL) {
 
+  value <- restoreInput(id = inputId, default = value)
+
   # build input tag
   inputTag <- tags$input(id = inputId, type = "number", class="form-control",
                          value = formatNoSci(value))
diff --git a/R/input-password.R b/R/input-password.R
index 58b9b88..2433881 100644
--- a/R/input-password.R
+++ b/R/input-password.R
@@ -9,12 +9,29 @@
 #' @seealso \code{\link{updateTextInput}}
 #'
 #' @examples
-#' passwordInput("password", "Password:")
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   passwordInput("password", "Password:"),
+#'   actionButton("go", "Go"),
+#'   verbatimTextOutput("value")
+#' )
+#' server <- function(input, output) {
+#'   output$value <- renderText({
+#'     req(input$go)
+#'     isolate(input$password)
+#'   })
+#' }
+#' shinyApp(ui, server)
+#' }
 #' @export
-passwordInput <- function(inputId, label, value = "", width = NULL) {
+passwordInput <- function(inputId, label, value = "", width = NULL,
+                          placeholder = NULL) {
   div(class = "form-group shiny-input-container",
     style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
     label %AND% tags$label(label, `for` = inputId),
-    tags$input(id = inputId, type="password", class="form-control", value=value)
+    tags$input(id = inputId, type="password", class="form-control", value=value,
+               placeholder = placeholder)
   )
 }
diff --git a/R/input-radiobuttons.R b/R/input-radiobuttons.R
index 7e1e085..72407f4 100644
--- a/R/input-radiobuttons.R
+++ b/R/input-radiobuttons.R
@@ -21,11 +21,33 @@
 #' @seealso \code{\link{updateRadioButtons}}
 #'
 #' @examples
-#' radioButtons("dist", "Distribution type:",
-#'              c("Normal" = "norm",
-#'                "Uniform" = "unif",
-#'                "Log-normal" = "lnorm",
-#'                "Exponential" = "exp"))
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   radioButtons("dist", "Distribution type:",
+#'                c("Normal" = "norm",
+#'                  "Uniform" = "unif",
+#'                  "Log-normal" = "lnorm",
+#'                  "Exponential" = "exp")),
+#'   plotOutput("distPlot")
+#' )
+#'
+#' server <- function(input, output) {
+#'   output$distPlot <- renderPlot({
+#'     dist <- switch(input$dist,
+#'                    norm = rnorm,
+#'                    unif = runif,
+#'                    lnorm = rlnorm,
+#'                    exp = rexp,
+#'                    rnorm)
+#'
+#'     hist(dist(500))
+#'   })
+#' }
+#'
+#' shinyApp(ui, server)
+#' }
 #' @export
 radioButtons <- function(inputId, label, choices, selected = NULL,
   inline = FALSE, width = NULL) {
@@ -33,6 +55,8 @@ radioButtons <- function(inputId, label, choices, selected = NULL,
   # resolve names
   choices <- choicesWithNames(choices)
 
+  selected <- restoreInput(id = inputId, default = selected)
+
   # default value if it's not specified
   selected <- if (is.null(selected)) choices[[1]] else {
     validateSelected(selected, choices, inputId)
diff --git a/R/input-select.R b/R/input-select.R
index 798415b..23e2201 100644
--- a/R/input-select.R
+++ b/R/input-select.R
@@ -31,14 +31,32 @@
 #' @seealso \code{\link{updateSelectInput}}
 #'
 #' @examples
-#' selectInput("variable", "Variable:",
-#'             c("Cylinders" = "cyl",
-#'               "Transmission" = "am",
-#'               "Gears" = "gear"))
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   selectInput("variable", "Variable:",
+#'               c("Cylinders" = "cyl",
+#'                 "Transmission" = "am",
+#'                 "Gears" = "gear")),
+#'   tableOutput("data")
+#' )
+#'
+#' server <- function(input, output) {
+#'   output$data <- renderTable({
+#'     mtcars[, c("mpg", input$variable), drop = FALSE]
+#'   }, rownames = TRUE)
+#' }
+#'
+#' shinyApp(ui, server)
+#' }
 #' @export
 selectInput <- function(inputId, label, choices, selected = NULL,
                         multiple = FALSE, selectize = TRUE, width = NULL,
                         size = NULL) {
+
+  selected <- restoreInput(id = inputId, default = selected)
+
   # resolve names
   choices <- choicesWithNames(choices)
 
@@ -154,7 +172,7 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
 
   if ('drag_drop' %in% options$plugins) {
     selectizeDep <- list(selectizeDep, htmlDependency(
-      'jqueryui', '1.11.4', c(href = 'shared/jqueryui'),
+      'jqueryui', '1.12.1', c(href = 'shared/jqueryui'),
       script = 'jquery-ui.min.js'
     ))
   }
diff --git a/R/input-slider.R b/R/input-slider.R
index bca54b4..b8d88f8 100644
--- a/R/input-slider.R
+++ b/R/input-slider.R
@@ -48,6 +48,27 @@
 #' @family input elements
 #' @seealso \code{\link{updateSliderInput}}
 #'
+#' @examples
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   sliderInput("obs", "Number of observations:",
+#'     min = 0, max = 1000, value = 500
+#'   ),
+#'   plotOutput("distPlot")
+#' )
+#'
+#' # Server logic
+#' server <- function(input, output) {
+#'   output$distPlot <- renderPlot({
+#'     hist(rnorm(input$obs))
+#'   })
+#' }
+#'
+#' # Complete app with UI and server components
+#' shinyApp(ui, server)
+#' }
 #' @export
 sliderInput <- function(inputId, label, min, max, value, step = NULL,
                         round = FALSE, format = NULL, locale = NULL,
@@ -64,6 +85,8 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
                     version = "0.10.2.2")
   }
 
+  value <- restoreInput(id = inputId, default = value)
+
   # If step is NULL, use heuristic to set the step size.
   findStepSize <- function(min, max, step) {
     if (!is.null(step)) return(step)
@@ -140,7 +163,6 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
     `data-grid` = ticks,
     `data-grid-num` = n_ticks,
     `data-grid-snap` = FALSE,
-    `data-prettify-separator` = sep,
     `data-prefix` = pre,
     `data-postfix` = post,
     `data-keyboard` = TRUE,
@@ -152,6 +174,12 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
     `data-timezone` = timezone
   ))
 
+  if (sep == "") {
+    sliderProps$`data-prettify-enabled` <- "0"
+  } else {
+    sliderProps$`data-prettify-separator` <- sep
+  }
+
   # Replace any TRUE and FALSE with "true" and "false"
   sliderProps <- lapply(sliderProps, function(x) {
     if (identical(x, TRUE)) "true"
@@ -191,7 +219,7 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
   }
 
   dep <- list(
-    htmlDependency("ionrangeslider", "2.0.12", c(href="shared/ionrangeslider"),
+    htmlDependency("ionrangeslider", "2.1.2", c(href="shared/ionrangeslider"),
       script = "js/ion.rangeSlider.min.js",
       # ion.rangeSlider also needs normalize.css, which is already included in
       # Bootstrap.
@@ -221,7 +249,6 @@ hasDecimals <- function(value) {
 #'   or list of tags (using \code{\link{tag}} and friends), or raw HTML (using
 #'   \code{\link{HTML}}).
 #' @param pauseButton Similar to \code{playButton}, but for the pause button.
-#'
 #' @export
 animationOptions <- function(interval=1000,
                              loop=FALSE,
diff --git a/R/input-text.R b/R/input-text.R
index 4a103d3..f45bc73 100644
--- a/R/input-text.R
+++ b/R/input-text.R
@@ -16,11 +16,24 @@
 #' @seealso \code{\link{updateTextInput}}
 #'
 #' @examples
-#' textInput("caption", "Caption:", "Data Summary")
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   textInput("caption", "Caption", "Data Summary"),
+#'   verbatimTextOutput("value")
+#' )
+#' server <- function(input, output) {
+#'   output$value <- renderText({ input$caption })
+#' }
+#' shinyApp(ui, server)
+#' }
 #' @export
 textInput <- function(inputId, label, value = "", width = NULL,
   placeholder = NULL) {
 
+  value <- restoreInput(id = inputId, default = value)
+
   div(class = "form-group shiny-input-container",
     style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
     label %AND% tags$label(label, `for` = inputId),
diff --git a/R/input-textarea.R b/R/input-textarea.R
new file mode 100644
index 0000000..cec1a82
--- /dev/null
+++ b/R/input-textarea.R
@@ -0,0 +1,69 @@
+#' Create a textarea input control
+#'
+#' Create a textarea input control for entry of unstructured text values.
+#'
+#' @inheritParams textInput
+#' @param height The height of the input, e.g. \code{'400px'}, or
+#'   \code{'100\%'}; see \code{\link{validateCssUnit}}.
+#' @param cols Value of the visible character columns of the input, e.g.
+#'   \code{80}. If used with \code{width}, \code{width} will take precedence in
+#'   the browser's rendering.
+#' @param rows The value of the visible character rows of the input, e.g.
+#'   \code{6}. If used with \code{height}, \code{height} will take precedence in
+#'   the browser's rendering.
+#' @param resize Which directions the textarea box can be resized. Can be one of
+#'   \code{"both"}, \code{"none"}, \code{"vertical"}, and \code{"horizontal"}.
+#'   The default, \code{NULL}, will use the client browser's default setting for
+#'   resizing textareas.
+#' @return A textarea input control that can be added to a UI definition.
+#'
+#' @family input elements
+#' @seealso \code{\link{updateTextAreaInput}}
+#'
+#' @examples
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   textAreaInput("caption", "Caption", "Data Summary", width = "1000px"),
+#'   verbatimTextOutput("value")
+#' )
+#' server <- function(input, output) {
+#'   output$value <- renderText({ input$caption })
+#' }
+#' shinyApp(ui, server)
+#'
+#' }
+#' @export
+textAreaInput <- function(inputId, label, value = "", width = NULL, height = NULL,
+  cols = NULL, rows = NULL, placeholder = NULL, resize = NULL) {
+
+  value <- restoreInput(id = inputId, default = value)
+
+  if (!is.null(resize)) {
+    resize <- match.arg(resize, c("both", "none", "vertical", "horizontal"))
+  }
+
+  style <- paste(
+    if (!is.null(width))  paste0("width: ",  validateCssUnit(width),  ";"),
+    if (!is.null(height)) paste0("height: ", validateCssUnit(height), ";"),
+    if (!is.null(resize)) paste0("resize: ", resize, ";")
+  )
+
+  # Workaround for tag attribute=character(0) bug:
+  #   https://github.com/rstudio/htmltools/issues/65
+  if (length(style) == 0) style <- NULL
+
+  div(class = "form-group shiny-input-container",
+    label %AND% tags$label(label, `for` = inputId),
+    tags$textarea(
+      id = inputId,
+      class = "form-control",
+      placeholder = placeholder,
+      style = style,
+      rows = rows,
+      cols = cols,
+      value
+    )
+  )
+}
diff --git a/R/input-utils.R b/R/input-utils.R
index 4a43f1a..65eb70b 100644
--- a/R/input-utils.R
+++ b/R/input-utils.R
@@ -4,10 +4,13 @@ controlLabel <- function(controlName, label) {
 
 
 # Before shiny 0.9, `selected` refers to names/labels of `choices`; now it
-# refers to values. Below is a function for backward compatibility.
+# refers to values. Below is a function for backward compatibility. It also
+# coerces the value to `character`.
 validateSelected <- function(selected, choices, inputId) {
-  # drop names, otherwise toJSON() keeps them too
-  selected <- unname(selected)
+  # this line accomplishes two tings:
+  #   - coerces selected to character
+  #   - drops name, otherwise toJSON() keeps it too
+  selected <- as.character(selected)
   # if you are using optgroups, you're using shiny > 0.10.0, and you should
   # already know that `selected` must be a value instead of a label
   if (needOptgroup(choices)) return(selected)
@@ -63,7 +66,7 @@ generateOptions <- function(inputId, choices, selected, inline, type = 'checkbox
 
 
 # Takes a vector or list, and adds names (same as the value) to any entries
-# without names.
+# without names. Coerces all leaf nodes to `character`.
 choicesWithNames <- function(choices) {
   # Take a vector or list, and convert to list. Also, if any children are
   # vectors with length > 1, convert those to list. If the list is unnamed,
@@ -79,7 +82,7 @@ choicesWithNames <- function(choices) {
       if (is.list(val))
         listify(val)
       else if (length(val) == 1 && is.null(names(val)))
-        val
+        as.character(val)
       else
         makeNamed(as.list(val))
     })
diff --git a/R/insert-ui.R b/R/insert-ui.R
new file mode 100644
index 0000000..9be2a2b
--- /dev/null
+++ b/R/insert-ui.R
@@ -0,0 +1,174 @@
+#' Insert UI objects
+#'
+#' Insert a UI object into the app.
+#'
+#' This function allows you to dynamically add an arbitrarily large UI
+#' object into your app, whenever you want, as many times as you want.
+#' Unlike \code{\link{renderUI}}, the UI generated with \code{insertUI}
+#' is not updatable as a whole: once it's created, it stays there. Each
+#' new call to \code{insertUI} creates more UI objects, in addition to
+#' the ones already there (all independent from one another). To
+#' update a part of the UI (ex: an input object), you must use the
+#' appropriate \code{render} function or a customized \code{reactive}
+#' function. To remove any part of your UI, use \code{\link{removeUI}}.
+#'
+#' @param selector A string that is accepted by jQuery's selector (i.e. the
+#' string \code{s} to be placed in a \code{$(s)} jQuery call). This selector
+#' will determine the element(s) relative to which you want to insert your
+#' UI object.
+#'
+#' @param where Where your UI object should go relative to the selector:
+#' \describe{
+#'   \item{\code{beforeBegin}}{Before the selector element itself}
+#'   \item{\code{afterBegin}}{Just inside the selector element, before its
+#'   first child}
+#'   \item{\code{beforeEnd}}{Just inside the selector element, after its
+#'   last child (default)}
+#'   \item{\code{afterEnd}}{After the selector element itself}
+#' }
+#' Adapted from
+#' \href{https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML}{here}.
+#'
+#' @param ui The UI object you want to insert. This can be anything that
+#' you usually put inside your apps's \code{ui} function. If you're inserting
+#' multiple elements in one call, make sure to wrap them in either a
+#' \code{tagList()} or a \code{tags$div()} (the latter option has the
+#' advantage that you can give it an \code{id} to make it easier to
+#' reference or remove it later on). If you want to insert raw html, use
+#' \code{ui = HTML()}.
+#'
+#' @param multiple In case your selector matches more than one element,
+#' \code{multiple} determines whether Shiny should insert the UI object
+#' relative to all matched elements or just relative to the first
+#' matched element (default).
+#'
+#' @param immediate Whether the UI object should be immediately inserted into
+#' the app when you call \code{insertUI}, or whether Shiny should wait until
+#' all outputs have been updated and all observers have been run (default).
+#'
+#' @param session The shiny session within which to call \code{insertUI}.
+#'
+#' @seealso \code{\link{removeUI}}
+#'
+#' @examples
+#' ## Only run this example in interactive R sessions
+#' if (interactive()) {
+#' # Define UI
+#' ui <- fluidPage(
+#'   actionButton("add", "Add UI")
+#' )
+#'
+#' # Server logic
+#' server <- function(input, output, session) {
+#'   observeEvent(input$add, {
+#'     insertUI(
+#'       selector = "#add",
+#'       where = "afterEnd",
+#'       ui = textInput(paste0("txt", input$add),
+#'                      "Insert some text")
+#'     )
+#'   })
+#' }
+#'
+#' # Complete app with UI and server components
+#' shinyApp(ui, server)
+#' }
+#' @export
+insertUI <- function(selector,
+  where = c("beforeBegin", "afterBegin", "beforeEnd", "afterEnd"),
+  ui,
+  multiple = FALSE,
+  immediate = FALSE,
+  session = getDefaultReactiveDomain()) {
+
+  force(selector)
+  force(ui)
+  force(session)
+  force(multiple)
+  if (missing(where)) where <- "beforeEnd"
+  where <- match.arg(where)
+
+  callback <- function() {
+    session$sendInsertUI(selector = selector,
+                         multiple = multiple,
+                         where = where,
+                         content = processDeps(ui, session))
+  }
+
+  if (!immediate) session$onFlushed(callback, once = TRUE)
+  else callback()
+}
+
+
+#' Remove UI objects
+#'
+#' Remove a UI object from the app.
+#'
+#' This function allows you to remove any part of your UI. Once \code{removeUI}
+#' is executed on some element, it is gone forever.
+#'
+#' While it may be a particularly useful pattern to pair this with
+#' \code{\link{insertUI}} (to remove some UI you had previously inserted),
+#' there is no restriction on what you can use \code{removeUI} on. Any
+#' element that can be selected through a jQuery selector can be removed
+#' through this function.
+#'
+#' @param selector A string that is accepted by jQuery's selector (i.e. the
+#' string \code{s} to be placed in a \code{$(s)} jQuery call). This selector
+#' will determine the element(s) to be removed. If you want to remove a
+#' Shiny input or output, note that many of these are wrapped in \code{div}s,
+#' so you may need to use a somewhat complex selector -- see the Examples below.
+#' (Alternatively, you could also wrap the inputs/outputs that you want to be
+#' able to remove easily in a \code{div} with an id.)
+#'
+#' @param multiple In case your selector matches more than one element,
+#' \code{multiple} determines whether Shiny should remove all the matched
+#' elements or just the first matched element (default).
+#'
+#' @param immediate Whether the element(s) should be immediately removed from
+#' the app when you call \code{removeUI}, or whether Shiny should wait until
+#' all outputs have been updated and all observers have been run (default).
+#'
+#' @param session The shiny session within which to call \code{removeUI}.
+#'
+#' @seealso \code{\link{insertUI}}
+#'
+#' @examples
+#' ## Only run this example in interactive R sessions
+#' if (interactive()) {
+#' # Define UI
+#' ui <- fluidPage(
+#'   actionButton("rmv", "Remove UI"),
+#'   textInput("txt", "This is no longer useful")
+#' )
+#'
+#' # Server logic
+#' server <- function(input, output, session) {
+#'   observeEvent(input$rmv, {
+#'     removeUI(
+#'       selector = "div:has(> #txt)"
+#'     )
+#'   })
+#' }
+#'
+#' # Complete app with UI and server components
+#' shinyApp(ui, server)
+#' }
+#' @export
+removeUI <- function(selector,
+  multiple = FALSE,
+  immediate = FALSE,
+  session = getDefaultReactiveDomain()) {
+
+  force(selector)
+  force(multiple)
+  force(session)
+
+  callback <- function() {
+    session$sendRemoveUI(selector = selector,
+                         multiple = multiple)
+  }
+
+  if (!immediate) session$onFlushed(callback, once = TRUE)
+  else callback()
+}
diff --git a/R/jqueryui.R b/R/jqueryui.R
index 77a35ff..bfe76b7 100644
--- a/R/jqueryui.R
+++ b/R/jqueryui.R
@@ -53,7 +53,6 @@
 #'   over text). The default is \code{"auto"}, which is equivalent to
 #'   \code{ifelse(draggable, "move", "inherit")}.
 #' @return An HTML element or list of elements.
-#'
 #' @export
 absolutePanel <- function(...,
                           top = NULL, left = NULL, right = NULL, bottom = NULL,
@@ -80,8 +79,6 @@ absolutePanel <- function(...,
   if (isTRUE(draggable)) {
     divTag <- tagAppendAttributes(divTag, class='draggable')
     return(tagList(
-      # IMPORTANT NOTE: If you update jqueryui, make sure you DON'T include the datepicker,
-      # as it collides with our bootstrap datepicker!
       singleton(tags$head(tags$script(src='shared/jqueryui/jquery-ui.min.js'))),
       divTag,
       tags$script('$(".draggable").draggable();')
diff --git a/R/middleware.R b/R/middleware.R
index 1b85807..6210f60 100644
--- a/R/middleware.R
+++ b/R/middleware.R
@@ -299,9 +299,7 @@ HandlerManager <- R6Class("HandlerManager",
 
           if (reqSize > maxSize) {
             return(list(status = 413L,
-              headers = list(
-                'Content-Type' = 'text/plain'
-              ),
+              headers = list('Content-Type' = 'text/plain'),
               body = 'Maximum upload size exceeded'))
           }
           else {
@@ -310,7 +308,18 @@ HandlerManager <- R6Class("HandlerManager",
         },
         call = .httpServer(
           function (req) {
-            withLogErrors(handlers$invoke(req))
+            withCallingHandlers(withLogErrors(handlers$invoke(req)),
+              error = function(cond) {
+                sanitizeErrors <- getOption('shiny.sanitize.errors', FALSE)
+                if (inherits(cond, 'shiny.custom.error') || !sanitizeErrors) {
+                  stop(cond$message, call. = FALSE)
+                } else {
+                  stop(paste("An error has occurred. Check your logs or",
+                             "contact the app author for clarification."),
+                       call. = FALSE)
+                }
+              }
+            )
           },
           getOption('shiny.sharedSecret')
         ),
@@ -333,7 +342,7 @@ HandlerManager <- R6Class("HandlerManager",
         }
 
         # Catch HEAD requests. For the purposes of handler functions, they
-        # should be treated like GET. The difference is that  they shouldn't
+        # should be treated like GET. The difference is that they shouldn't
         # return a body in the http response.
         head_request <- FALSE
         if (identical(req$REQUEST_METHOD, "HEAD")) {
diff --git a/R/modal.R b/R/modal.R
new file mode 100644
index 0000000..ecf0adb
--- /dev/null
+++ b/R/modal.R
@@ -0,0 +1,183 @@
+#' Show or remove a modal dialog
+#'
+#' This causes a modal dialog to be displayed in the client browser, and is
+#' typically used with \code{\link{modalDialog}}.
+#'
+#' @param ui UI content to show in the modal.
+#' @param session The \code{session} object passed to function given to
+#'   \code{shinyServer}.
+#'
+#' @seealso \code{\link{modalDialog}} for examples.
+#' @export
+showModal <- function(ui, session = getDefaultReactiveDomain()) {
+  res <- processDeps(ui, session)
+
+  session$sendModal("show",
+    list(
+      html = res$html,
+      deps = res$deps
+    )
+  )
+}
+
+#' @rdname showModal
+#' @export
+removeModal <- function(session = getDefaultReactiveDomain()) {
+  session$sendModal("remove", NULL)
+}
+
+
+#' Create a modal dialog UI
+#'
+#' This creates the UI for a modal dialog, using Bootstrap's modal class. Modals
+#' are typically used for showing important messages, or for presenting UI that
+#' requires input from the user, such as a username and password input.
+#'
+#' @param ... UI elements for the body of the modal dialog box.
+#' @param title An optional title for the dialog.
+#' @param footer UI for footer. Use \code{NULL} for no footer.
+#' @param size One of \code{"s"} for small, \code{"m"} (the default) for medium,
+#'   or \code{"l"} for large.
+#' @param easyClose If \code{TRUE}, the modal dialog can be dismissed by
+#'   clicking outside the dialog box, or be pressing the Escape key. If
+#'   \code{FALSE} (the default), the modal dialog can't be dismissed in those
+#'   ways; instead it must be dismissed by clicking on the dismiss button, or
+#'   from a call to \code{\link{removeModal}} on the server.
+#' @param fade If \code{FALSE}, the modal dialog will have no fade-in animation
+#'   (it will simply appear rather than fade in to view).
+#'
+#' @examples
+#' if (interactive()) {
+#' # Display an important message that can be dismissed only by clicking the
+#' # dismiss button.
+#' shinyApp(
+#'   ui = basicPage(
+#'     actionButton("show", "Show modal dialog")
+#'   ),
+#'   server = function(input, output) {
+#'     observeEvent(input$show, {
+#'       showModal(modalDialog(
+#'         title = "Important message",
+#'         "This is an important message!"
+#'       ))
+#'     })
+#'   }
+#' )
+#'
+#'
+#' # Display a message that can be dismissed by clicking outside the modal dialog,
+#' # or by pressing Esc.
+#' shinyApp(
+#'   ui = basicPage(
+#'     actionButton("show", "Show modal dialog")
+#'   ),
+#'   server = function(input, output) {
+#'     observeEvent(input$show, {
+#'       showModal(modalDialog(
+#'         title = "Somewhat important message",
+#'         "This is a somewhat important message.",
+#'         easyClose = TRUE,
+#'         footer = NULL
+#'       ))
+#'     })
+#'   }
+#' )
+#'
+#'
+#' # Display a modal that requires valid input before continuing.
+#' shinyApp(
+#'   ui = basicPage(
+#'     actionButton("show", "Show modal dialog"),
+#'     verbatimTextOutput("dataInfo")
+#'   ),
+#'
+#'   server = function(input, output) {
+#'     # reactiveValues object for storing current data set.
+#'     vals <- reactiveValues(data = NULL)
+#'
+#'     # Return the UI for a modal dialog with data selection input. If 'failed' is
+#'     # TRUE, then display a message that the previous value was invalid.
+#'     dataModal <- function(failed = FALSE) {
+#'       modalDialog(
+#'         textInput("dataset", "Choose data set",
+#'           placeholder = 'Try "mtcars" or "abc"'
+#'         ),
+#'         span('(Try the name of a valid data object like "mtcars", ',
+#'              'then a name of a non-existent object like "abc")'),
+#'         if (failed)
+#'           div(tags$b("Invalid name of data object", style = "color: red;")),
+#'
+#'         footer = tagList(
+#'           modalButton("Cancel"),
+#'           actionButton("ok", "OK")
+#'         )
+#'       )
+#'     }
+#'
+#'     # Show modal when button is clicked.
+#'     observeEvent(input$show, {
+#'       showModal(dataModal())
+#'     })
+#'
+#'     # When OK button is pressed, attempt to load the data set. If successful,
+#'     # remove the modal. If not show another modal, but this time with a failure
+#'     # message.
+#'     observeEvent(input$ok, {
+#'       # Check that data object exists and is data frame.
+#'       if (!is.null(input$dataset) && nzchar(input$dataset) &&
+#'           exists(input$dataset) && is.data.frame(get(input$dataset))) {
+#'         vals$data <- get(input$dataset)
+#'         removeModal()
+#'       } else {
+#'         showModal(dataModal(failed = TRUE))
+#'       }
+#'     })
+#'
+#'     # Display information about selected data
+#'     output$dataInfo <- renderPrint({
+#'       if (is.null(vals$data))
+#'         "No data selected"
+#'       else
+#'         summary(vals$data)
+#'     })
+#'   }
+#' )
+#' }
+#' @export
+modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"),
+  size = c("m", "s", "l"), easyClose = FALSE, fade = TRUE) {
+
+  size <- match.arg(size)
+
+  cls <- if (fade) "modal fade" else "modal"
+  div(id = "shiny-modal", class = cls, tabindex = "-1",
+    `data-backdrop` = if (!easyClose) "static",
+    `data-keyboard` = if (!easyClose) "false",
+
+    div(
+      class = "modal-dialog",
+      class = switch(size, s = "modal-sm", m = NULL, l = "modal-lg"),
+      div(class = "modal-content",
+        if (!is.null(title)) div(class = "modal-header",
+          tags$h4(class = "modal-title", title)
+        ),
+        div(class = "modal-body", ...),
+        if (!is.null(footer)) div(class = "modal-footer", footer)
+      )
+    ),
+    tags$script("$('#shiny-modal').modal().focus();")
+  )
+}
+
+#' Create a button for a modal dialog
+#'
+#' When clicked, a \code{modalButton} will dismiss the modal dialog.
+#'
+#' @inheritParams actionButton
+#' @seealso \code{\link{modalDialog}} for examples.
+#' @export
+modalButton <- function(label, icon = NULL) {
+  tags$button(type = "button", class = "btn btn-default",
+    `data-dismiss` = "modal", validateIcon(icon), label
+  )
+}
diff --git a/R/modules.R b/R/modules.R
index f8ec2ca..ee5d742 100644
--- a/R/modules.R
+++ b/R/modules.R
@@ -1,4 +1,4 @@
-# Creates an object whose $ and $<- pass through to the parent
+# Creates an object whose $ and [[ pass through to the parent
 # session, unless the name is matched in ..., in which case
 # that value is returned instead. (See Decorator pattern.)
 createSessionProxy <- function(parentSession, ...) {
@@ -14,18 +14,24 @@ createSessionProxy <- function(parentSession, ...) {
 
 #' @export
 `$.session_proxy` <- function(x, name) {
-  if (name %in% names(x[["overrides"]]))
-    x[["overrides"]][[name]]
+  if (name %in% names(.subset2(x, "overrides")))
+    .subset2(x, "overrides")[[name]]
   else
-    x[["parent"]][[name]]
+    .subset2(x, "parent")[[name]]
 }
 
 #' @export
+`[[.session_proxy` <- `$.session_proxy`
+
+
+#' @export
 `$<-.session_proxy` <- function(x, name, value) {
-  x[["parent"]][[name]] <- value
-  x
+  stop("Attempted to assign value on session proxy.")
 }
 
+`[[<-.session_proxy` <- `$<-.session_proxy`
+
+
 #' Invoke a Shiny module
 #'
 #' Shiny's module feature lets you break complicated UI and server logic into
@@ -42,7 +48,6 @@ createSessionProxy <- function(parentSession, ...) {
 #'
 #' @return The return value, if any, from executing the module server function
 #' @seealso \url{http://shiny.rstudio.com/articles/modules.html}
-#'
 #' @export
 callModule <- function(module, id, ..., session = getDefaultReactiveDomain()) {
   childScope <- session$makeScope(id)
diff --git a/R/notifications.R b/R/notifications.R
new file mode 100644
index 0000000..0d35225
--- /dev/null
+++ b/R/notifications.R
@@ -0,0 +1,106 @@
+#' Show or remove a notification
+#'
+#' These functions show and remove notifications in a Shiny application.
+#'
+#' @param ui Content of message.
+#' @param action Message content that represents an action. For example, this
+#'   could be a link that the user can click on. This is separate from \code{ui}
+#'   so customized layouts can handle the main notification content separately
+#'   from action content.
+#' @param duration Number of seconds to display the message before it
+#'   disappears. Use \code{NULL} to make the message not automatically
+#'   disappear.
+#' @param closeButton If \code{TRUE}, display a button which will make the
+#'   notification disappear when clicked. If \code{FALSE} do not display.
+#' @param id An ID string. This can be used to change the contents of an
+#'   existing message with \code{showNotification}, or to remove it with
+#'   \code{removeNotification}. If not provided, one will be generated
+#'   automatically. If an ID is provided and there does not currently exist a
+#'   notification with that ID, a new notification will be created with that ID.
+#' @param type A string which controls the color of the notification. One of
+#'   "default" (gray), "message" (blue), "warning" (yellow), or "error" (red).
+#' @param session Session object to send notification to.
+#'
+#' @return An ID for the notification.
+#'
+#' @examples
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#' # Show a message when button is clicked
+#' shinyApp(
+#'   ui = fluidPage(
+#'     actionButton("show", "Show")
+#'   ),
+#'   server = function(input, output) {
+#'     observeEvent(input$show, {
+#'       showNotification("Message text",
+#'         action = a(href = "javascript:location.reload();", "Reload page")
+#'       )
+#'     })
+#'   }
+#' )
+#'
+#' # App with show and remove buttons
+#' shinyApp(
+#'   ui = fluidPage(
+#'     actionButton("show", "Show"),
+#'     actionButton("remove", "Remove")
+#'   ),
+#'   server = function(input, output) {
+#'     # A queue of notification IDs
+#'     ids <- character(0)
+#'     # A counter
+#'     n <- 0
+#'
+#'     observeEvent(input$show, {
+#'       # Save the ID for removal later
+#'       id <- showNotification(paste("Message", n), duration = NULL)
+#'       ids <<- c(ids, id)
+#'       n <<- n + 1
+#'     })
+#'
+#'     observeEvent(input$remove, {
+#'       if (length(ids) > 0)
+#'         removeNotification(ids[1])
+#'       ids <<- ids[-1]
+#'     })
+#'   }
+#' )
+#' }
+#' @export
+showNotification <- function(ui, action = NULL, duration = 5,
+  closeButton = TRUE, id = NULL,
+  type = c("default", "message", "warning", "error"),
+  session = getDefaultReactiveDomain())
+{
+
+  if (is.null(id))
+    id <- createUniqueId(8)
+
+  res <- processDeps(ui, session)
+  actionRes <- processDeps(action, session)
+
+  session$sendNotification("show",
+    list(
+      html = res$html,
+      action = actionRes$html,
+      deps = c(res$deps, actionRes$deps),
+      duration = if (!is.null(duration)) duration * 1000,
+      closeButton = closeButton,
+      id = id,
+      type = match.arg(type)
+    )
+  )
+
+  id
+}
+
+#' @rdname showNotification
+#' @export
+removeNotification <- function(id = NULL, session = getDefaultReactiveDomain()) {
+  if (is.null(id)) {
+    stop("id is required.")
+  }
+  session$sendNotification("remove", id)
+  id
+}
diff --git a/R/progress.R b/R/progress.R
index 8499a87..98d5b0a 100644
--- a/R/progress.R
+++ b/R/progress.R
@@ -12,6 +12,14 @@
 #' method is called. Calling \code{close} will cause the progress panel
 #' to be removed.
 #'
+#' As of version 0.14, the progress indicators use Shiny's new notification API.
+#' If you want to use the old styling (for example, you may have used customized
+#' CSS), you can use \code{style="old"} each time you call
+#' \code{Progress$new()}. If you don't want to set the style each time
+#' \code{Progress$new} is called, you can instead call
+#' \code{\link{shinyOptions}(progress.style="old")} just once, inside the server
+#' function.
+#'
 #' \strong{Methods}
 #'   \describe{
 #'     \item{\code{initialize(session, min = 0, max = 1)}}{
@@ -48,6 +56,10 @@
 #' @param value A numeric value at which to set
 #'   the progress bar, relative to \code{min} and \code{max}.
 #'   \code{NULL} hides the progress bar, if it is currently visible.
+#' @param style Progress display style. If \code{"notification"} (the default),
+#'   the progress indicator will show using Shiny's notification API. If
+#'   \code{"old"}, use the same HTML and CSS used in Shiny 0.13.2 and below
+#'   (this is for backward-compatibility).
 #' @param amount Single-element numeric vector; the value at which to set
 #'   the progress bar, relative to \code{min} and \code{max}.
 #'   \code{NULL} hides the progress bar, if it is currently visible.
@@ -55,11 +67,16 @@
 #'   progress bar.
 #'
 #' @examples
-#' \dontrun{
-#' # server.R
-#' shinyServer(function(input, output, session) {
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   plotOutput("plot")
+#' )
+#'
+#' server <- function(input, output, session) {
 #'   output$plot <- renderPlot({
-#'     progress <- shiny::Progress$new(session, min=1, max=15)
+#'     progress <- Progress$new(session, min=1, max=15)
 #'     on.exit(progress$close())
 #'
 #'     progress$set(message = 'Calculation in progress',
@@ -71,7 +88,9 @@
 #'     }
 #'     plot(cars)
 #'   })
-#' })
+#' }
+#'
+#' shinyApp(ui, server)
 #' }
 #' @seealso \code{\link{withProgress}}
 #' @format NULL
@@ -82,18 +101,22 @@ Progress <- R6Class(
   portable = TRUE,
   public = list(
 
-    initialize = function(session = getDefaultReactiveDomain(), min = 0, max = 1) {
+    initialize = function(session = getDefaultReactiveDomain(),
+      min = 0, max = 1,
+      style = getShinyOption("progress.style", default = "notification"))
+    {
       if (is.null(session$progressStack))
         stop("'session' is not a ShinySession object.")
 
       private$session <- session
-      private$id <- paste(as.character(as.raw(stats::runif(8, min=0, max=255))), collapse='')
+      private$id <- createUniqueId(8)
       private$min <- min
       private$max <- max
+      private$style <- match.arg(style, choices = c("notification", "old"))
       private$value <- NULL
       private$closed <- FALSE
 
-      session$sendProgress('open', list(id = private$id))
+      session$sendProgress('open', list(id = private$id, style = private$style))
     },
 
     set = function(value = NULL, message = NULL, detail = NULL) {
@@ -115,7 +138,8 @@ Progress <- R6Class(
         id = private$id,
         message = message,
         detail = detail,
-        value = value
+        value = value,
+        style = private$style
       ))
 
        private$session$sendProgress('update', data)
@@ -141,7 +165,9 @@ Progress <- R6Class(
         return()
       }
 
-      private$session$sendProgress('close', list(id = private$id))
+      private$session$sendProgress('close',
+        list(id = private$id, style = private$style)
+      )
       private$closed <- TRUE
     }
   ),
@@ -151,6 +177,7 @@ Progress <- R6Class(
     id = character(0),
     min = numeric(0),
     max = numeric(0),
+    style = character(0),
     value = NULL,
     closed = logical(0)
   )
@@ -179,6 +206,14 @@ Progress <- R6Class(
 #' is not common) or otherwise cannot be encapsulated by a single scope. In that
 #' case, you can use the \code{Progress} reference class.
 #'
+#' As of version 0.14, the progress indicators use Shiny's new notification API.
+#' If you want to use the old styling (for example, you may have used customized
+#' CSS), you can use \code{style="old"} each time you call
+#' \code{withProgress()}. If you don't want to set the style each time
+#' \code{withProgress} is called, you can instead call
+#' \code{\link{shinyOptions}(progress.style="old")} just once, inside the server
+#' function.
+#'
 #' @param session The Shiny session object, as provided by \code{shinyServer} to
 #'   the server function. The default is to automatically find the session by
 #'   using the current reactive domain.
@@ -199,14 +234,23 @@ Progress <- R6Class(
 #'   displayed to the user, or \code{NULL} to hide the current detail message
 #'   (if any). The detail message will be shown with a de-emphasized appearance
 #'   relative to \code{message}.
+#' @param style Progress display style. If \code{"notification"} (the default),
+#'   the progress indicator will show using Shiny's notification API. If
+#'   \code{"old"}, use the same HTML and CSS used in Shiny 0.13.2 and below
+#'   (this is for backward-compatibility).
 #' @param value Single-element numeric vector; the value at which to set the
 #'   progress bar, relative to \code{min} and \code{max}. \code{NULL} hides the
 #'   progress bar, if it is currently visible.
 #'
 #' @examples
-#' \dontrun{
-#' # server.R
-#' shinyServer(function(input, output) {
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   plotOutput("plot")
+#' )
+#'
+#' server <- function(input, output) {
 #'   output$plot <- renderPlot({
 #'     withProgress(message = 'Calculation in progress',
 #'                  detail = 'This may take a while...', value = 0, {
@@ -217,16 +261,20 @@ Progress <- R6Class(
 #'     })
 #'     plot(cars)
 #'   })
-#' })
+#' }
+#'
+#' shinyApp(ui, server)
 #' }
 #' @seealso \code{\link{Progress}}
 #' @rdname withProgress
 #' @export
 withProgress <- function(expr, min = 0, max = 1,
-                         value = min + (max - min) * 0.1,
-                         message = NULL, detail = NULL,
-                         session = getDefaultReactiveDomain(),
-                         env = parent.frame(), quoted = FALSE) {
+  value = min + (max - min) * 0.1,
+  message = NULL, detail = NULL,
+  style = getShinyOption("progress.style", default = "notification"),
+  session = getDefaultReactiveDomain(),
+  env = parent.frame(), quoted = FALSE)
+{
 
   if (!quoted)
     expr <- substitute(expr)
@@ -234,7 +282,9 @@ withProgress <- function(expr, min = 0, max = 1,
   if (is.null(session$progressStack))
     stop("'session' is not a ShinySession object.")
 
-  p <- Progress$new(session, min = min, max = max)
+  style <- match.arg(style, c("notification", "old"))
+
+  p <- Progress$new(session, min = min, max = max, style = style)
 
   session$progressStack$push(p)
   on.exit({
diff --git a/R/react.R b/R/react.R
index e8d18ff..75febcc 100644
--- a/R/react.R
+++ b/R/react.R
@@ -115,13 +115,16 @@ ReactiveEnvironment <- R6Class(
     addPendingFlush = function(ctx, priority) {
       .pendingFlush$enqueue(ctx, priority)
     },
+    hasPendingFlush = function() {
+      return(!.pendingFlush$isEmpty())
+    },
     flush = function() {
       # If already in a flush, don't start another one
       if (.inFlush) return()
       .inFlush <<- TRUE
       on.exit(.inFlush <<- FALSE)
 
-      while (!.pendingFlush$isEmpty()) {
+      while (hasPendingFlush()) {
         ctx <- .pendingFlush$dequeue()
         ctx$executeFlushCallbacks()
       }
diff --git a/R/reactive-domains.R b/R/reactive-domains.R
index 02665ee..29cb600 100644
--- a/R/reactive-domains.R
+++ b/R/reactive-domains.R
@@ -42,11 +42,11 @@ NULL
 #
 ## ------------------------------------------------------------------------
 createMockDomain <- function() {
-  callbacks <- list()
+  callbacks <- Callbacks$new()
   ended <- FALSE
   domain <- new.env(parent = emptyenv())
   domain$onEnded <- function(callback) {
-    callbacks <<- c(callbacks, callback)
+    return(callbacks$register(callback))
   }
   domain$isEnded <- function() {
     ended
@@ -55,7 +55,7 @@ createMockDomain <- function() {
   domain$end <- function() {
     if (!ended) {
       ended <<- TRUE
-      lapply(callbacks, do.call, list())
+      callbacks$invoke()
     }
     invisible()
   }
diff --git a/R/reactives.R b/R/reactives.R
index e89a8ce..e9b84ff 100644
--- a/R/reactives.R
+++ b/R/reactives.R
@@ -47,6 +47,7 @@ ReactiveValues <- R6Class(
     # For debug purposes
     .label = character(0),
     .values = 'environment',
+    .metadata = 'environment',
     .dependents = 'environment',
     # Dependents for the list of all names, including hidden
     .namesDeps = 'Dependents',
@@ -60,32 +61,40 @@ ReactiveValues <- R6Class(
                        p_randomInt(1000, 10000),
                        sep="")
       .values <<- new.env(parent=emptyenv())
+      .metadata <<- new.env(parent=emptyenv())
       .dependents <<- new.env(parent=emptyenv())
       .namesDeps <<- Dependents$new()
       .allValuesDeps <<- Dependents$new()
       .valuesDeps <<- Dependents$new()
     },
+
     get = function(key) {
+      # Register the "downstream" reactive which is accessing this value, so
+      # that we know to invalidate them when this value changes.
       ctx <- .getReactiveEnvironment()$currentContext()
       dep.key <- paste(key, ':', ctx$id, sep='')
-      if (!exists(dep.key, where=.dependents, inherits=FALSE)) {
+      if (!exists(dep.key, envir=.dependents, inherits=FALSE)) {
         .graphDependsOn(ctx$id, sprintf('%s$%s', .label, key))
-        assign(dep.key, ctx, pos=.dependents, inherits=FALSE)
+        .dependents[[dep.key]] <- ctx
         ctx$onInvalidate(function() {
-          rm(list=dep.key, pos=.dependents, inherits=FALSE)
+          rm(list=dep.key, envir=.dependents, inherits=FALSE)
         })
       }
 
-      if (!exists(key, where=.values, inherits=FALSE))
+      if (isFrozen(key))
+        reactiveStop()
+
+      if (!exists(key, envir=.values, inherits=FALSE))
         NULL
       else
-        base::get(key, pos=.values, inherits=FALSE)
+        .values[[key]]
     },
+
     set = function(key, value) {
       hidden <- substr(key, 1, 1) == "."
 
-      if (exists(key, where=.values, inherits=FALSE)) {
-        if (identical(base::get(key, pos=.values, inherits=FALSE), value)) {
+      if (exists(key, envir=.values, inherits=FALSE)) {
+        if (identical(.values[[key]], value)) {
           return(invisible())
         }
       }
@@ -98,14 +107,14 @@ ReactiveValues <- R6Class(
       else
         .valuesDeps$invalidate()
 
-      assign(key, value, pos=.values, inherits=FALSE)
+      .values[[key]] <- value
 
       .graphValueChange(sprintf('names(%s)', .label), ls(.values, all.names=TRUE))
       .graphValueChange(sprintf('%s (all)', .label), as.list(.values))
       .graphValueChange(sprintf('%s$%s', .label, key), value)
 
       dep.keys <- objects(
-        pos=.dependents,
+        envir=.dependents,
         pattern=paste('^\\Q', key, ':', '\\E', '\\d+$', sep=''),
         all.names=TRUE
       )
@@ -118,18 +127,54 @@ ReactiveValues <- R6Class(
       )
       invisible()
     },
+
     mset = function(lst) {
       lapply(base::names(lst),
              function(name) {
                self$set(name, lst[[name]])
              })
     },
+
     names = function() {
       .graphDependsOn(.getReactiveEnvironment()$currentContext()$id,
                       sprintf('names(%s)', .label))
       .namesDeps$register()
       return(ls(.values, all.names=TRUE))
     },
+
+    # Get a metadata value. Does not trigger reactivity.
+    getMeta = function(key, metaKey) {
+      # Make sure to use named (not numeric) indexing into list.
+      metaKey <- as.character(metaKey)
+      .metadata[[key]][[metaKey]]
+    },
+
+    # Set a metadata value. Does not trigger reactivity.
+    setMeta = function(key, metaKey, value) {
+      # Make sure to use named (not numeric) indexing into list.
+      metaKey <- as.character(metaKey)
+
+      if (!exists(key, envir = .metadata, inherits = FALSE)) {
+        .metadata[[key]] <<- list()
+      }
+
+      .metadata[[key]][[metaKey]] <<- value
+    },
+
+    # Mark a value as frozen If accessed while frozen, a shiny.silent.error will
+    # be thrown.
+    freeze = function(key) {
+      setMeta(key, "frozen", TRUE)
+    },
+
+    thaw = function(key) {
+      setMeta(key, "frozen", NULL)
+    },
+
+    isFrozen = function(key) {
+      isTRUE(getMeta(key, "frozen"))
+    },
+
     toList = function(all.names=FALSE) {
       .graphDependsOn(.getReactiveEnvironment()$currentContext()$id,
                       sprintf('%s (all)', .label))
@@ -140,6 +185,7 @@ ReactiveValues <- R6Class(
 
       return(as.list(.values, all.names=all.names))
     },
+
     .setLabel = function(label) {
       .label <<- label
     }
@@ -186,7 +232,6 @@ ReactiveValues <- R6Class(
 #'   these objects must be named.
 #'
 #' @seealso \code{\link{isolate}} and \code{\link{is.reactivevalues}}.
-#'
 #' @export
 reactiveValues <- function(...) {
   args <- list(...)
@@ -317,10 +362,25 @@ as.list.reactivevalues <- function(x, all.names=FALSE, ...) {
 #' # isolate() can also be used when calling from outside a reactive context (e.g.
 #' # at the console)
 #' isolate(reactiveValuesToList(values))
-#'
 #' @export
 reactiveValuesToList <- function(x, all.names=FALSE) {
-  .subset2(x, 'impl')$toList(all.names)
+  # Default case
+  res <- .subset2(x, 'impl')$toList(all.names)
+
+  prefix <- .subset2(x, 'ns')("")
+  # Special handling for namespaces
+  if (nzchar(prefix)) {
+    fullNames <- names(res)
+
+    # Filter out items that match namespace
+    fullNames <- fullNames[substring(fullNames, 1, nchar(prefix)) == prefix]
+    res <- res[fullNames]
+
+    # Remove namespace prefix
+    names(res) <- substring(fullNames, nchar(prefix) + 1)
+  }
+
+  res
 }
 
 # This function is needed because str() on a reactivevalues object will call
@@ -334,6 +394,67 @@ str.reactivevalues <- function(object, indent.str = " ", ...) {
   utils::str(class(object))
 }
 
+
+#' Freeze a reactive value
+#'
+#' This freezes a reactive value. If the value is accessed while frozen, a
+#' "silent" exception is raised and the operation is stopped. This is the same
+#' thing that happens if \code{req(FALSE)} is called. The value is thawed
+#' (un-frozen; accessing it will no longer raise an exception) when the current
+#' reactive domain is flushed. In a Shiny application, this occurs after all of
+#' the observers are executed.
+#'
+#' @param x A \code{\link{reactiveValues}} object (like \code{input}).
+#' @param name The name of a value in the \code{\link{reactiveValues}} object.
+#'
+#' @seealso \code{\link{req}}
+#' @examples
+#' ## Only run this examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   selectInput("data", "Data Set", c("mtcars", "pressure")),
+#'   checkboxGroupInput("cols", "Columns (select 2)", character(0)),
+#'   plotOutput("plot")
+#' )
+#'
+#' server <- function(input, output, session) {
+#'   observe({
+#'     data <- get(input$data)
+#'     # Sets a flag on input$cols to essentially do req(FALSE) if input$cols
+#'     # is accessed. Without this, an error will momentarily show whenever a
+#'     # new data set is selected.
+#'     freezeReactiveValue(input, "cols")
+#'     updateCheckboxGroupInput(session, "cols", choices = names(data))
+#'   })
+#'
+#'   output$plot <- renderPlot({
+#'     # When a new data set is selected, input$cols will have been invalidated
+#'     # above, and this will essentially do the same as req(FALSE), causing
+#'     # this observer to stop and raise a silent exception.
+#'     cols <- input$cols
+#'     data <- get(input$data)
+#'
+#'     if (length(cols) == 2) {
+#'       plot(data[[ cols[1] ]], data[[ cols[2] ]])
+#'     }
+#'   })
+#' }
+#'
+#' shinyApp(ui, server)
+#' }
+#' @export
+freezeReactiveValue <- function(x, name) {
+  domain <- getDefaultReactiveDomain()
+  if (is.null(getDefaultReactiveDomain)) {
+    stop("freezeReactiveValue() must be called when a default reactive domain is active.")
+  }
+
+  domain$freezeValue(x, name)
+  invisible()
+}
+
+
 # Observable ----------------------------------------------------------------
 
 Observable <- R6Class(
@@ -360,7 +481,16 @@ Observable <- R6Class(
              "or more parameters; only functions without parameters can be ",
              "reactive.")
 
-      .func <<- wrapFunctionLabel(func, paste("reactive", label),
+      # This is to make sure that the function labels that show in the profiler
+      # and in stack traces doesn't contain whitespace. See
+      # https://github.com/rstudio/profvis/issues/58
+      if (grepl("\\s", label, perl = TRUE)) {
+        funcLabel <- "<reactive>"
+      } else {
+        funcLabel <- paste0("<reactive:", label, ">")
+      }
+
+      .func <<- wrapFunctionLabel(func, funcLabel,
         ..stacktraceon = ..stacktraceon)
       .label <<- label
       .domain <<- domain
@@ -490,7 +620,6 @@ Observable <- R6Class(
 #' isolate(reactiveB())
 #' isolate(reactiveC())
 #' isolate(reactiveD())
-#'
 #' @export
 reactive <- function(x, env = parent.frame(), quoted = FALSE, label = NULL,
                      domain = getDefaultReactiveDomain(),
@@ -553,7 +682,7 @@ srcrefToLabel <- function(srcref, defaultLabel) {
 
 #' @export
 print.reactive <- function(x, ...) {
-  label <- attr(x, "observable")$.label
+  label <- attr(x, "observable", exact = TRUE)$.label
   cat(label, "\n")
 }
 
@@ -564,7 +693,7 @@ is.reactive <- function(x) inherits(x, "reactive")
 # Return the number of times that a reactive expression or observer has been run
 execCount <- function(x) {
   if (is.reactive(x))
-    return(attr(x, "observable")$.execCount)
+    return(attr(x, "observable", exact = TRUE)$.execCount)
   else if (inherits(x, 'Observer'))
     return(x$.execCount)
   else
@@ -582,12 +711,18 @@ Observer <- R6Class(
     .domain = 'ANY',
     .priority = numeric(0),
     .autoDestroy = logical(0),
+    # A function that, when invoked, unsubscribes the autoDestroy
+    # listener (or NULL if autodestroy is disabled for this observer).
+    # We must unsubscribe when this observer is destroyed, or else
+    # the observer cannot be garbage collected until the session ends.
+    .autoDestroyHandle = 'ANY',
     .invalidateCallbacks = list(),
     .execCount = integer(0),
     .onResume = 'function',
     .suspended = logical(0),
     .destroyed = logical(0),
     .prevId = character(0),
+    .ctx = NULL,
 
     initialize = function(observerFunc, label, suspended = FALSE, priority = 0,
                           domain = getDefaultReactiveDomain(),
@@ -602,15 +737,14 @@ registerDebugHook("observerFunc", environment(), label)
             ..stacktraceon..(observerFunc())
           else
             observerFunc(),
-          validation = function(e) {
-            # It's OK for a validation error to cause an observer to stop
-            # running
-          }
+          # It's OK for shiny.silent.error errors to cause an observer to stop running
+          shiny.silent.error = function(e) NULL
+          # validation = function(e) NULL,
+          # shiny.output.cancel = function(e) NULL
         )
       }
       .label <<- label
       .domain <<- domain
-      .autoDestroy <<- autoDestroy
       .priority <<- normalizePriority(priority)
       .execCount <<- 0L
       .suspended <<- suspended
@@ -618,7 +752,9 @@ registerDebugHook("observerFunc", environment(), label)
       .destroyed <<- FALSE
       .prevId <<- ''
 
-      onReactiveDomainEnded(.domain, self$.onDomainEnded)
+      .autoDestroy <<- FALSE
+      .autoDestroyHandle <<- NULL
+      setAutoDestroy(autoDestroy)
 
       # Defer the first running of this until flushReact is called
       .createContext()$invalidate()
@@ -627,7 +763,23 @@ registerDebugHook("observerFunc", environment(), label)
       ctx <- Context$new(.domain, .label, type='observer', prevId=.prevId)
       .prevId <<- ctx$id
 
+      if (!is.null(.ctx)) {
+        # If this happens, something went wrong.
+        warning("Created a new context without invalidating previous context.")
+      }
+      # Store the context explicitly in the Observer object. This is necessary
+      # to make sure that when the observer is destroyed, it also gets
+      # invalidated. Otherwise the upstream reactive (on which the observer
+      # depends) will hold a (indirect) reference to this context until the
+      # reactive is invalidated, which may not happen immediately or at all.
+      # This can lead to a memory leak (#1253).
+      .ctx <<- ctx
+
       ctx$onInvalidate(function() {
+        # Context is invalidated, so we don't need to store a reference to it
+        # anymore.
+        .ctx <<- NULL
+
         lapply(.invalidateCallbacks, function(invalidateCallback) {
           invalidateCallback()
           NULL
@@ -680,11 +832,28 @@ registerDebugHook("observerFunc", environment(), label)
       "Sets whether this observer should be automatically destroyed when its
       domain (if any) ends. If autoDestroy is TRUE and the domain already
       ended, then destroy() is called immediately."
+
+      if (.autoDestroy == autoDestroy) {
+        return(.autoDestroy)
+      }
+
       oldValue <- .autoDestroy
       .autoDestroy <<- autoDestroy
-      if (!is.null(.domain) && .domain$isEnded()) {
-        destroy()
+
+      if (autoDestroy) {
+        if (!.destroyed && !is.null(.domain)) { # Make sure to not try to destroy twice.
+          if (.domain$isEnded()) {
+            destroy()
+          } else {
+            .autoDestroyHandle <<- onReactiveDomainEnded(.domain, .onDomainEnded)
+          }
+        }
+      } else {
+        if (!is.null(.autoDestroyHandle))
+          .autoDestroyHandle()
+        .autoDestroyHandle <<- NULL
       }
+
       invisible(oldValue)
     },
     suspend = function() {
@@ -710,8 +879,21 @@ registerDebugHook("observerFunc", environment(), label)
       "Prevents this observer from ever executing again (even if a flush has
       already been scheduled)."
 
+      # Make sure to not try to destory twice.
+      if (.destroyed)
+        return()
+
       suspend()
       .destroyed <<- TRUE
+
+      if (!is.null(.autoDestroyHandle)) {
+        .autoDestroyHandle()
+      }
+      .autoDestroyHandle <<- NULL
+
+      if (!is.null(.ctx)) {
+        .ctx$invalidate()
+      }
     },
     .onDomainEnded = function() {
       if (isTRUE(.autoDestroy)) {
@@ -740,8 +922,8 @@ registerDebugHook("observerFunc", environment(), label)
 #' soon as their dependencies change, they schedule themselves to re-execute.
 #'
 #' Starting with Shiny 0.10.0, observers are automatically destroyed by default
-#' when the \link[=domains]{domain} that owns them ends (e.g. when a Shiny session
-#' ends).
+#' when the \link[=domains]{domain} that owns them ends (e.g. when a Shiny
+#' session ends).
 #'
 #' @param x An expression (quoted or unquoted). Any return value will be
 #'   ignored.
@@ -752,12 +934,13 @@ registerDebugHook("observerFunc", environment(), label)
 #'   This is useful when you want to use an expression that is stored in a
 #'   variable; to do so, it must be quoted with \code{quote()}.
 #' @param label A label for the observer, useful for debugging.
-#' @param suspended If \code{TRUE}, start the observer in a suspended state.
-#'   If \code{FALSE} (the default), start in a non-suspended state.
+#' @param suspended If \code{TRUE}, start the observer in a suspended state. If
+#'   \code{FALSE} (the default), start in a non-suspended state.
 #' @param priority An integer or numeric that controls the priority with which
-#'   this observer should be executed. An observer with a given priority level
-#'   will always execute sooner than all observers with a lower priority level.
-#'   Positive, negative, and zero values are allowed.
+#'   this observer should be executed. A higher value means higher priority: an
+#'   observer with a higher priority value will execute before all observers
+#'   with lower priority values. Positive, negative, and zero values are
+#'   allowed.
 #' @param domain See \link{domains}.
 #' @param autoDestroy If \code{TRUE} (the default), the observer will be
 #'   automatically destroyed when its domain (if any) ends.
@@ -816,7 +999,6 @@ registerDebugHook("observerFunc", environment(), label)
 #' # In a normal Shiny app, the web client will trigger flush events. If you
 #' # are at the console, you can force a flush with flushReact()
 #' shiny:::flushReact()
-#'
 #' @export
 observe <- function(x, env=parent.frame(), quoted=FALSE, label=NULL,
                     suspended=FALSE, priority=0,
@@ -856,9 +1038,9 @@ observe <- function(x, env=parent.frame(), quoted=FALSE, label=NULL,
 #' }
 #' @export
 makeReactiveBinding <- function(symbol, env = parent.frame()) {
-  if (exists(symbol, where = env, inherits = FALSE)) {
-    initialValue <- get(symbol, pos = env, inherits = FALSE)
-    rm(list = symbol, pos = env, inherits = FALSE)
+  if (exists(symbol, envir = env, inherits = FALSE)) {
+    initialValue <- env[[symbol]]
+    rm(list = symbol, envir = env, inherits = FALSE)
   }
   else
     initialValue <- NULL
@@ -931,8 +1113,15 @@ setAutoflush <- local({
 #' @seealso \code{\link{invalidateLater}}
 #'
 #' @examples
-#' \dontrun{
-#' shinyServer(function(input, output, session) {
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   sliderInput("n", "Number of observations", 2, 1000, 500),
+#'   plotOutput("plot")
+#' )
+#'
+#' server <- function(input, output) {
 #'
 #'   # Anything that calls autoInvalidate will automatically invalidate
 #'   # every 2 seconds.
@@ -953,11 +1142,12 @@ setAutoflush <- local({
 #'   # input$n changes.
 #'   output$plot <- renderPlot({
 #'     autoInvalidate()
-#'     hist(isolate(input$n))
+#'     hist(rnorm(isolate(input$n)))
 #'   })
-#' })
 #' }
 #'
+#' shinyApp(ui, server)
+#' }
 #' @export
 reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain()) {
   dependents <- Map$new()
@@ -1009,8 +1199,15 @@ reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain())
 #' @seealso \code{\link{reactiveTimer}} is a slightly less safe alternative.
 #'
 #' @examples
-#' \dontrun{
-#' shinyServer(function(input, output, session) {
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   sliderInput("n", "Number of observations", 2, 1000, 500),
+#'   plotOutput("plot")
+#' )
+#'
+#' server <- function(input, output, session) {
 #'
 #'   observe({
 #'     # Re-execute this reactive expression after 1000 milliseconds
@@ -1027,11 +1224,12 @@ reactiveTimer <- function(intervalMs=1000, session = getDefaultReactiveDomain())
 #'   output$plot <- renderPlot({
 #'     # Re-execute this reactive expression after 2000 milliseconds
 #'     invalidateLater(2000)
-#'     hist(isolate(input$n))
+#'     hist(rnorm(isolate(input$n)))
 #'   })
-#' })
 #' }
 #'
+#' shinyApp(ui, server)
+#' }
 #' @export
 invalidateLater <- function(millis, session = getDefaultReactiveDomain()) {
   ctx <- .getReactiveEnvironment()$currentContext()
@@ -1101,16 +1299,13 @@ coerceToFunc <- function(x) {
 #' @seealso \code{\link{reactiveFileReader}}
 #'
 #' @examples
-#' \dontrun{
 #' # Assume the existence of readTimestamp and readValue functions
-#' shinyServer(function(input, output, session) {
+#' function(input, output, session) {
 #'   data <- reactivePoll(1000, session, readTimestamp, readValue)
 #'   output$dataTable <- renderTable({
 #'     data()
 #'   })
-#' })
 #' }
-#'
 #' @export
 reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
   intervalMillis <- coerceToFunc(intervalMillis)
@@ -1170,7 +1365,7 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
 #' @examples
 #' \dontrun{
 #' # Per-session reactive file reader
-#' shinyServer(function(input, output, session)) {
+#' function(input, output, session) {
 #'   fileData <- reactiveFileReader(1000, session, 'data.csv', read.csv)
 #'
 #'   output$data <- renderTable({
@@ -1182,13 +1377,12 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
 #' # the same reader, so read.csv only gets executed once no matter how many
 #' # user sessions are connected.
 #' fileData <- reactiveFileReader(1000, session, 'data.csv', read.csv)
-#' shinyServer(function(input, output, session)) {
+#' function(input, output, session) {
 #'   output$data <- renderTable({
 #'     fileData()
 #'   })
 #' }
 #' }
-#'
 #' @export
 reactiveFileReader <- function(intervalMillis, session, filePath, readFunc, ...) {
   filePath <- coerceToFunc(filePath)
@@ -1276,7 +1470,6 @@ reactiveFileReader <- function(intervalMillis, session, filePath, readFunc, ...)
 #'
 #' # isolate also works if the reactive expression accesses values from the
 #' # input object, like input$x
-#'
 #' @export
 isolate <- function(expr) {
   ctx <- Context$new(getDefaultReactiveDomain(), '[isolate]', type='isolate')
@@ -1298,7 +1491,6 @@ isolate <- function(expr) {
 #' @return The value of \code{expr}.
 #'
 #' @seealso \code{\link{isolate}}
-#'
 #' @export
 maskReactiveContext <- function(expr) {
   .getReactiveEnvironment()$runWith(NULL, function() {
@@ -1425,7 +1617,6 @@ maskReactiveContext <- function(expr) {
 #'   }
 #'   shinyApp(ui=ui, server=server)
 #' }
-#'
 #' @export
 observeEvent <- function(eventExpr, handlerExpr,
   event.env = parent.frame(), event.quoted = FALSE,
diff --git a/R/render-plot.R b/R/render-plot.R
index 9db50b0..94e2b74 100644
--- a/R/render-plot.R
+++ b/R/render-plot.R
@@ -35,12 +35,20 @@
 #' @param env The environment in which to evaluate \code{expr}.
 #' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This
 #'   is useful if you want to save an expression in a variable.
-#' @param func A function that generates a plot (deprecated; use \code{expr}
-#'   instead).
-#'
+#' @param execOnResize If \code{FALSE} (the default), then when a plot is
+#'   resized, Shiny will \emph{replay} the plot drawing commands with
+#'   \code{\link[grDevices]{replayPlot}()} instead of re-executing \code{expr}.
+#'   This can result in faster plot redrawing, but there may be rare cases where
+#'   it is undesirable. If you encounter problems when resizing a plot, you can
+#'   have Shiny re-execute the code on resize by setting this to \code{TRUE}.
+#' @param outputArgs A list of arguments to be passed through to the implicit
+#'   call to \code{\link{plotOutput}} when \code{renderPlot} is used in an
+#'   interactive R Markdown document.
 #' @export
 renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
-                       env=parent.frame(), quoted=FALSE, func=NULL) {
+                       env=parent.frame(), quoted=FALSE,
+                       execOnResize=FALSE, outputArgs=list()
+) {
   # This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
   # is called
   installExprFunction(expr, "func", env, quoted, ..stacktraceon = TRUE)
@@ -50,66 +58,172 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
   if (is.function(width))
     widthWrapper <- reactive({ width() })
   else
-    widthWrapper <- NULL
+    widthWrapper <- function() { width }
 
   if (is.function(height))
     heightWrapper <- reactive({ height() })
   else
-    heightWrapper <- NULL
+    heightWrapper <- function() { height }
+
+  # A modified version of print.ggplot which returns the built ggplot object
+  # as well as the gtable grob. This overrides the ggplot::print.ggplot
+  # method, but only within the context of renderPlot. The reason this needs
+  # to be a (pseudo) S3 method is so that, if an object has a class in
+  # addition to ggplot, and there's a print method for that class, that we
+  # won't override that method. https://github.com/rstudio/shiny/issues/841
+  print.ggplot <- function(x) {
+    grid::grid.newpage()
 
-  # If renderPlot isn't going to adapt to the height of the div, then the
-  # div needs to adapt to the height of renderPlot. By default, plotOutput
-  # sets the height to 400px, so to make it adapt we need to override it
-  # with NULL.
-  outputFunc <- plotOutput
-  if (!identical(height, 'auto')) formals(outputFunc)['height'] <- list(NULL)
+    build <- ggplot2::ggplot_build(x)
 
-  return(markRenderFunction(outputFunc, function(shinysession, name, ...) {
-    if (!is.null(widthWrapper))
-      width <- widthWrapper()
-    if (!is.null(heightWrapper))
-      height <- heightWrapper()
+    gtable <- ggplot2::ggplot_gtable(build)
+    grid::grid.draw(gtable)
+
+    structure(list(
+      build = build,
+      gtable = gtable
+    ), class = "ggplot_build_gtable")
+  }
+
+
+  getDims <- function() {
+    width <- widthWrapper()
+    height <- heightWrapper()
 
     # 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).
-    prefix <- 'output_'
     if (width == 'auto')
-      width <- shinysession$clientData[[paste(prefix, name, '_width', sep='')]];
+      width <- session$clientData[[paste0('output_', outputName, '_width')]]
     if (height == 'auto')
-      height <- shinysession$clientData[[paste(prefix, name, '_height', sep='')]];
+      height <- session$clientData[[paste0('output_', outputName, '_height')]]
+
+    list(width = width, height = height)
+  }
+
+  # Vars to store session and output, so that they can be accessed from
+  # the plotObj() reactive.
+  session <- NULL
+  outputName <- NULL
+
+  # This function is the one that's returned from renderPlot(), and gets
+  # wrapped in an observer when the output value is assigned. The expression
+  # passed to renderPlot() is actually run in plotObj(); this function can only
+  # replay a plot if the width/height changes.
+  renderFunc <- function(shinysession, name, ...) {
+    session <<- shinysession
+    outputName <<- name
 
-    if (is.null(width) || is.null(height) || width <= 0 || height <= 0)
+    dims <- getDims()
+
+    if (is.null(dims$width) || is.null(dims$height) ||
+        dims$width <= 0 || dims$height <= 0) {
       return(NULL)
+    }
+
+    # The reactive that runs the expr in renderPlot()
+    plotData <- plotObj()
+
+    img <- plotData$img
+
+    # If only the width/height have changed, simply replay the plot and make a
+    # new img.
+    if (dims$width != img$width || dims$height != img$height) {
+      pixelratio <- session$clientData$pixelratio %OR% 1
+
+      coordmap <- NULL
+      plotFunc <- function() {
+        ..stacktraceon..(grDevices::replayPlot(plotData$recordedPlot))
+
+        # Coordmap must be recalculated after replaying plot, because pixel
+        # dimensions will have changed.
+        if (inherits(plotData$plotResult, "ggplot_build_gtable")) {
+          coordmap <<- getGgplotCoordmap(plotData$plotResult, pixelratio, res)
+        } else {
+          coordmap <<- getPrevPlotCoordmap(dims$width, dims$height)
+        }
+      }
+      outfile <- ..stacktraceoff..(
+        plotPNG(plotFunc, width = dims$width*pixelratio, height = dims$height*pixelratio,
+                res = res*pixelratio)
+      )
+      on.exit(unlink(outfile))
+
+      img <- dropNulls(list(
+        src = session$fileUrl(name, outfile, contentType='image/png'),
+        width = dims$width,
+        height = dims$height,
+        coordmap = coordmap,
+        # Get coordmap error message if present
+        error = attr(coordmap, "error", exact = TRUE)
+      ))
+    }
+
+    img
+  }
+
+
+  plotObj <- reactive(label = "plotObj", {
+    if (execOnResize) {
+      dims <- getDims()
+    } else {
+      isolate({ dims <- getDims() })
+    }
+
+    if (is.null(dims$width) || is.null(dims$height) ||
+        dims$width <= 0 || dims$height <= 0) {
+      return(NULL)
+    }
 
     # Resolution multiplier
-    pixelratio <- shinysession$clientData$pixelratio
-    if (is.null(pixelratio))
-      pixelratio <- 1
+    pixelratio <- session$clientData$pixelratio %OR% 1
 
+    plotResult <- NULL
+    recordedPlot <- NULL
     coordmap <- NULL
     plotFunc <- function() {
-      # Actually perform the plotting
-      result <- withVisible(func())
-
-      coordmap <<- NULL
+      success <-FALSE
+      tryCatch(
+        {
+          # This is necessary to enable displaylist recording
+          grDevices::dev.control(displaylist = "enable")
+
+          # Actually perform the plotting
+          result <- withVisible(func())
+          success <- TRUE
+        },
+        finally = {
+          if (!success) {
+            # If there was an error in making the plot, there's a good chance
+            # it's "Error in plot.new: figure margins too large". We need to
+            # take a reactive dependency on the width and height, so that the
+            # user's plotting code will re-execute when the plot is resized,
+            # instead of just replaying the previous plot (which errored).
+            getDims()
+          }
+        }
+      )
 
       if (result$visible) {
         # Use capture.output to squelch printing to the actual console; we
         # are only interested in plot output
-
-        # Special case for ggplot objects - need to capture coordmap
-        if (inherits(result$value, "ggplot")) {
-          utils::capture.output(coordmap <<- getGgplotCoordmap(result$value, pixelratio))
-        } else {
-          # This ..stacktraceon.. negates the ..stacktraceoff.. that wraps the
-          # call to plotFunc
-          utils::capture.output(..stacktraceon..(print(result$value)))
-        }
+        utils::capture.output({
+          # This ..stacktraceon.. negates the ..stacktraceoff.. that wraps
+          # the call to plotFunc. The value needs to be printed just in case
+          # it's an object that requires printing to generate plot output,
+          # similar to ggplot2. But for base graphics, it would already have
+          # been rendered when func was called above, and the print should
+          # have no effect.
+          plotResult <<- ..stacktraceon..(print(result$value))
+        })
       }
 
-      if (is.null(coordmap)) {
-        coordmap <<- getPrevPlotCoordmap(width, height)
+      recordedPlot <<- grDevices::recordPlot()
+
+      if (inherits(plotResult, "ggplot_build_gtable")) {
+        coordmap <<- getGgplotCoordmap(plotResult, pixelratio, res)
+      } else {
+        coordmap <<- getPrevPlotCoordmap(dims$width, dims$height)
       }
     }
 
@@ -118,25 +232,38 @@ renderPlot <- function(expr, width='auto', height='auto', res=72, ...,
     # renderPlot, and by the ..stacktraceon.. in plotFunc where ggplot objects
     # are printed
     outfile <- ..stacktraceoff..(
-      do.call(plotPNG, c(plotFunc, width=width*pixelratio,
-        height=height*pixelratio, res=res*pixelratio, args))
+      do.call(plotPNG, c(plotFunc, width=dims$width*pixelratio,
+        height=dims$height*pixelratio, res=res*pixelratio, args))
     )
     on.exit(unlink(outfile))
 
-    # A list of attributes for the img
-    res <- list(
-      src=shinysession$fileUrl(name, outfile, contentType='image/png'),
-      width=width, height=height, coordmap=coordmap
+    list(
+      # img is the content that gets sent to the client.
+      img = dropNulls(list(
+        src = session$fileUrl(outputName, outfile, contentType='image/png'),
+        width = dims$width,
+        height = dims$height,
+        coordmap = coordmap,
+        # Get coordmap error message if present.
+        error = attr(coordmap, "error", exact = TRUE)
+      )),
+      # Returned value from expression in renderPlot() -- may be a printable
+      # object like ggplot2. Needed just in case we replayPlot and need to get
+      # a coordmap again.
+      plotResult = plotResult,
+      recordedPlot = recordedPlot
     )
+  })
 
-    # Get error message if present (from attribute on the coordmap)
-    error <- attr(coordmap, "error", exact = TRUE)
-    if (!is.null(error)) {
-      res$error <- error
-    }
 
-    res
-  }))
+  # If renderPlot isn't going to adapt to the height of the div, then the
+  # div needs to adapt to the height of renderPlot. By default, plotOutput
+  # sets the height to 400px, so to make it adapt we need to override it
+  # with NULL.
+  outputFunc <- plotOutput
+  if (!identical(height, 'auto')) formals(outputFunc)['height'] <- list(NULL)
+
+  markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
 }
 
 # The coordmap extraction functions below return something like the examples
@@ -288,61 +415,42 @@ getPrevPlotCoordmap <- function(width, height) {
   ))
 }
 
-# Print a ggplot object and return a coordmap for it.
-getGgplotCoordmap <- function(p, pixelratio) {
-  if (!inherits(p, "ggplot"))
-    return(NULL)
 
-  # A modified version of print.ggplot which returns the built ggplot object as
-  # well as the gtable grob. This overrides the ggplot::print.ggplot method, but
-  # only within the context of getGgplotCoordmap. The reason this needs to be an
-  # (pseudo) S3 method is so that, if an object has a class in addition to
-  # ggplot, and there's a print method for that class, that we won't override
-  # that method.
-  # https://github.com/rstudio/shiny/issues/841
-  print.ggplot <- function(x) {
-    grid::grid.newpage()
-
-    build <- ggplot2::ggplot_build(x)
-
-    gtable <- ggplot2::ggplot_gtable(build)
-    grid::grid.draw(gtable)
-
-    list(
-      build = build,
-      gtable = gtable
-    )
-  }
-
-  # Given the name of a generic function and an object, return the class name
-  # for the method that would be used on the object.
-  which_method <- function(generic, x) {
-    classes <- class(x)
-    method_names <- paste(generic, classes, sep = ".")
-    idx <- which(method_names %in% utils::methods(generic))
-
-    if (length(idx) == 0)
-      return(NULL)
-
-    # Return name of first class with matching method
-    classes[idx[1]]
-  }
+# Given a ggplot_build_gtable object, return a coordmap for it.
+getGgplotCoordmap <- function(p, pixelratio, res) {
+  # Structure of ggplot objects changed after 2.1.0
+  new_ggplot <- (utils::packageVersion("ggplot2") > "2.1.0")
 
+  if (!inherits(p, "ggplot_build_gtable"))
+    return(NULL)
 
   # Given a built ggplot object, return x and y domains (data space coords) for
   # each panel.
   find_panel_info <- function(b) {
-    layout <- b$panel$layout
+    if (new_ggplot) {
+      layout <- b$layout$panel_layout
+    } else {
+      layout <- b$panel$layout
+    }
     # Convert factor to numbers
     layout$PANEL <- as.integer(as.character(layout$PANEL))
 
     # Names of facets
-    facet <- b$plot$facet
     facet_vars <- NULL
-    if (inherits(facet, "grid")) {
-      facet_vars <- vapply(c(facet$cols, facet$rows), as.character, character(1))
-    } else if (inherits(facet, "wrap")) {
-      facet_vars <- vapply(facet$facets, as.character, character(1))
+    if (new_ggplot) {
+      facet <- b$layout$facet
+      if (inherits(facet, "FacetGrid")) {
+        facet_vars <- vapply(c(facet$params$cols, facet$params$rows), as.character, character(1))
+      } else if (inherits(facet, "FacetWrap")) {
+        facet_vars <- vapply(facet$params$facets, as.character, character(1))
+      }
+    } else {
+      facet <- b$plot$facet
+      if (inherits(facet, "grid")) {
+        facet_vars <- vapply(c(facet$cols, facet$rows), as.character, character(1))
+      } else if (inherits(facet, "wrap")) {
+        facet_vars <- vapply(facet$facets, as.character, character(1))
+      }
     }
 
     # Iterate over each row in the layout data frame
@@ -384,8 +492,12 @@ getGgplotCoordmap <- function(p, pixelratio) {
   # Given a single range object (representing the data domain) from a built
   # ggplot object, return the domain.
   find_panel_domain <- function(b, panel_num, scalex_num = 1, scaley_num = 1) {
-    range <- b$panel$ranges[[panel_num]]
-    res <- list(
+    if (new_ggplot) {
+      range <- b$layout$panel_ranges[[panel_num]]
+    } else {
+      range <- b$panel$ranges[[panel_num]]
+    }
+    domain <- list(
       left   = range$x.range[1],
       right  = range$x.range[2],
       bottom = range$y.range[1],
@@ -393,19 +505,23 @@ getGgplotCoordmap <- function(p, pixelratio) {
     )
 
     # Check for reversed scales
-    xscale <- b$panel$x_scales[[scalex_num]]
-    yscale <- b$panel$y_scales[[scaley_num]]
-
+    if (new_ggplot) {
+      xscale <- b$layout$panel_scales$x[[scalex_num]]
+      yscale <- b$layout$panel_scales$y[[scaley_num]]
+    } else {
+      xscale <- b$panel$x_scales[[scalex_num]]
+      yscale <- b$panel$y_scales[[scaley_num]]
+    }
     if (!is.null(xscale$trans) && xscale$trans$name == "reverse") {
-      res$left  <- -res$left
-      res$right <- -res$right
+      domain$left  <- -domain$left
+      domain$right <- -domain$right
     }
     if (!is.null(yscale$trans) && yscale$trans$name == "reverse") {
-      res$top    <- -res$top
-      res$bottom <- -res$bottom
+      domain$top    <- -domain$top
+      domain$bottom <- -domain$bottom
     }
 
-    res
+    domain
   }
 
   # Given built ggplot object, return object with the log base for x and y if
@@ -430,10 +546,18 @@ getGgplotCoordmap <- function(p, pixelratio) {
     y_names <- character(0)
 
     # Continuous scales have a trans; discrete ones don't
-    if (!is.null(b$panel$x_scales[[scalex_num]]$trans))
-      x_names <- b$panel$x_scales[[scalex_num]]$trans$name
-    if (!is.null(b$panel$y_scales[[scaley_num]]$trans))
-      y_names <- b$panel$y_scales[[scaley_num]]$trans$name
+    if (new_ggplot) {
+      if (!is.null(b$layout$panel_scales$x[[scalex_num]]$trans))
+        x_names <- b$layout$panel_scales$x[[scalex_num]]$trans$name
+      if (!is.null(b$layout$panel_scales$y[[scaley_num]]$trans))
+        y_names <- b$layout$panel_scales$y[[scaley_num]]$trans$name
+
+    } else {
+      if (!is.null(b$panel$x_scales[[scalex_num]]$trans))
+        x_names <- b$panel$x_scales[[scalex_num]]$trans$name
+      if (!is.null(b$panel$y_scales[[scaley_num]]$trans))
+        y_names <- b$panel$y_scales[[scaley_num]]$trans$name
+    }
 
     coords <- b$plot$coordinates
     if (!is.null(coords$trans)) {
@@ -487,6 +611,11 @@ getGgplotCoordmap <- function(p, pixelratio) {
       )
     }
 
+    # Look for CoordFlip
+    if (inherits(b$plot$coordinates, "CoordFlip")) {
+      mappings[c("x", "y")] <- mappings[c("y", "x")]
+    }
+
     mappings_cache <<- mappings
     mappings
   }
@@ -513,9 +642,22 @@ getGgplotCoordmap <- function(p, pixelratio) {
       }
     }
 
+    # Workaround for a bug in the quartz device. If you have a 400x400 image and
+    # run `convertWidth(unit(1, "npc"), "native")`, the result will depend on
+    # res setting of the device. If res=72, then it returns 400 (as expected),
+    # but if, e.g., res=96, it will return 300, which is incorrect.
+    devScaleFactor <- 1
+    if (grepl("quartz", names(grDevices::dev.cur()), fixed = TRUE)) {
+      devScaleFactor <- res / 72
+    }
+
     # Convert a unit (or vector of units) to a numeric vector of pixel sizes
-    h_px <- function(x) as.numeric(grid::convertHeight(x, "native"))
-    w_px <- function(x) as.numeric(grid::convertWidth(x, "native"))
+    h_px <- function(x) {
+      devScaleFactor * grid::convertHeight(x, "native", valueOnly = TRUE)
+    }
+    w_px <- function(x) {
+      devScaleFactor * grid::convertWidth(x, "native", valueOnly = TRUE)
+    }
 
     # Given a vector of relative sizes (in grid units), and a function for
     # converting grid units to numeric pixels, return a numeric vector of
@@ -582,36 +724,25 @@ getGgplotCoordmap <- function(p, pixelratio) {
     })
   }
 
-  # If print(p) gets dispatched to print.ggplot(p), attempt to extract coordmap.
-  # If dispatched to another method, just print the object and don't attempt to
-  # extract the coordmap. This can happen if there's another print method that
-  # takes precedence.
-  if (identical(which_method("print", p), "ggplot")) {
-    res <- print(p)
-
-    tryCatch({
-      # Get info from built ggplot object
-      info <- find_panel_info(res$build)
 
-      # Get ranges from gtable - it's possible for this to return more elements than
-      # info, because it calculates positions even for panels that aren't present.
-      # This can happen with facet_wrap.
-      ranges <- find_panel_ranges(res$gtable, pixelratio)
+  tryCatch({
+    # Get info from built ggplot object
+    info <- find_panel_info(p$build)
 
-      for (i in seq_along(info)) {
-        info[[i]]$range <- ranges[[i]]
-      }
+    # Get ranges from gtable - it's possible for this to return more elements than
+    # info, because it calculates positions even for panels that aren't present.
+    # This can happen with facet_wrap.
+    ranges <- find_panel_ranges(p$gtable, pixelratio)
 
-      return(info)
+    for (i in seq_along(info)) {
+      info[[i]]$range <- ranges[[i]]
+    }
 
-    }, error = function(e) {
-      # If there was an error extracting info from the ggplot object, just return
-      # a list with the error message.
-      return(structure(list(), error = e$message))
-    })
+    return(info)
 
-  } else {
-    print(p)
-    return(list())
-  }
+  }, error = function(e) {
+    # If there was an error extracting info from the ggplot object, just return
+    # a list with the error message.
+    return(structure(list(), error = e$message))
+  })
 }
diff --git a/R/render-table.R b/R/render-table.R
index 775f53a..1d3128d 100644
--- a/R/render-table.R
+++ b/R/render-table.R
@@ -3,57 +3,221 @@
 #' Creates a reactive table that is suitable for assigning to an \code{output}
 #' slot.
 #'
-#' The corresponding HTML output tag should be \code{div} and have the CSS class
-#' name \code{shiny-html-output}.
+#' The corresponding HTML output tag should be \code{div} and have the CSS
+#' class name \code{shiny-html-output}.
 #'
 #' @param expr An expression 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}} and
-#'   \code{\link[xtable]{print.xtable}}.
+#' @param striped,hover,bordered Logicals: if \code{TRUE}, apply the
+#'   corresponding Bootstrap table format to the output table.
+#' @param spacing The spacing between the rows of the table (\code{xs}
+#'   stands for "extra small", \code{s} for "small", \code{m} for "medium"
+#'   and \code{l} for "large").
+#' @param width Table width. Must be a valid CSS unit (like "100%", "400px",
+#'   "auto") or a number, which will be coerced to a string and
+#'   have "px" appended.
+#' @param align A string that specifies the column alignment. If equal to
+#'   \code{'l'}, \code{'c'} or \code{'r'}, then all columns will be,
+#'   respectively, left-, center- or right-aligned. Otherwise, \code{align}
+#'   must have the same number of characters as the resulting table (if
+#'   \code{rownames = TRUE}, this will be equal to \code{ncol()+1}), with
+#'   the \emph{i}-th character specifying the alignment for the
+#'   \emph{i}-th column (besides \code{'l'}, \code{'c'} and
+#'   \code{'r'}, \code{'?'} is also permitted - \code{'?'} is a placeholder
+#'   for that particular column, indicating that it should keep its default
+#'   alignment). If \code{NULL}, then all numeric/integer columns (including
+#'   the row names, if they are numbers) will be right-aligned and
+#'   everything else will be left-aligned (\code{align = '?'} produces the
+#'   same result).
+#' @param rownames,colnames Logicals: include rownames? include colnames
+#'   (column headers)?
+#' @param digits An integer specifying the number of decimal places for
+#'   the numeric columns (this will not apply to columns with an integer
+#'   class). If \code{digits} is set to a negative value, then the numeric
+#'   columns will be displayed in scientific format with a precision of
+#'   \code{abs(digits)} digits.
+#' @param na The string to use in the table cells whose values are missing
+#'   (i.e. they either evaluate to \code{NA} or \code{NaN}).
+#' @param ... Arguments to be passed through to \code{\link[xtable]{xtable}}
+#'   and \code{\link[xtable]{print.xtable}}.
 #' @param env The environment in which to evaluate \code{expr}.
-#' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This
-#'   is useful if you want to save an expression in a variable.
-#' @param func A function that returns an R object that can be used with
-#'   \code{\link[xtable]{xtable}} (deprecated; use \code{expr} instead).
-#'
+#' @param quoted Is \code{expr} a quoted expression (with \code{quote()})?
+#'   This is useful if you want to save an expression in a variable.
+#' @param outputArgs A list of arguments to be passed through to the
+#'   implicit call to \code{\link{tableOutput}} when \code{renderTable} is
+#'   used in an interactive R Markdown document.
 #' @export
-renderTable <- function(expr, ..., env=parent.frame(), quoted=FALSE, func=NULL) {
-  if (!is.null(func)) {
-    shinyDeprecated(msg="renderTable: argument 'func' is deprecated. Please use 'expr' instead.")
-  } else {
-    installExprFunction(expr, "func", env, quoted)
+renderTable <- function(expr, striped = FALSE, hover = FALSE,
+                        bordered = FALSE, spacing = c("s", "xs", "m", "l"),
+                        width = "auto", align = NULL,
+                        rownames = FALSE, colnames = TRUE,
+                        digits = NULL, na = "NA", ...,
+                        env = parent.frame(), quoted = FALSE,
+                        outputArgs=list()) {
+  installExprFunction(expr, "func", env, quoted)
+
+  if (!is.function(spacing)) spacing <- match.arg(spacing)
+
+  # A small helper function to create a wrapper for an argument that was
+  # passed to renderTable()
+  createWrapper <- function(arg) {
+    if (is.function(arg)) wrapper <- arg
+    else wrapper <- function() arg
+    return(wrapper)
   }
 
-  markRenderFunction(tableOutput, function() {
-    classNames <- getOption('shiny.table.class') %OR% 'data table table-bordered table-condensed'
+  # Create wrappers for most arguments so that functions can also be passed
+  # in, rather than only literals (useful for shiny apps)
+  stripedWrapper <- createWrapper(striped)
+  hoverWrapper <- createWrapper(hover)
+  borderedWrapper <- createWrapper(bordered)
+  spacingWrapper <- createWrapper(spacing)
+  widthWrapper <- createWrapper(width)
+  alignWrapper <- createWrapper(align)
+  rownamesWrapper <- createWrapper(rownames)
+  colnamesWrapper <- createWrapper(colnames)
+  digitsWrapper <- createWrapper(digits)
+  naWrapper <- createWrapper(na)
+
+  dots <- list(...)  ## used later (but defined here because of scoping)
+
+  renderFunc <- function(shinysession, name, ...) {
+    striped <- stripedWrapper()
+    hover <- hoverWrapper()
+    bordered <- borderedWrapper()
+    format <- c(striped = striped, hover = hover, bordered = bordered)
+    spacing <- spacingWrapper()
+    width <- widthWrapper()
+    align <- alignWrapper()
+    rownames <- rownamesWrapper()
+    colnames <- colnamesWrapper()
+    digits <- digitsWrapper()
+    na <- naWrapper()
+
+    spacing_choices <- c("s", "xs", "m", "l")
+    if (!(spacing %in% spacing_choices)) {
+      stop(paste("`spacing` must be one of",
+                 paste0("'", spacing_choices, "'", collapse=", ")))
+    }
+
+    # For css styling
+    classNames <- paste0("table shiny-table",
+                         paste0(" table-", names(format)[format], collapse = "" ),
+                         paste0(" spacing-", spacing))
+
     data <- func()
+    data <- as.data.frame(data)
 
-    if (is.null(data) || identical(data, data.frame()))
-      return("")
+    # Return NULL if no data is provided
+    if (is.null(data) ||
+        (is.data.frame(data) && nrow(data) == 0 && ncol(data) == 0))
+      return(NULL)
 
     # Separate the ... args to pass to xtable() vs print.xtable()
-    dots <- list(...)
     xtable_argnames <- setdiff(names(formals(xtable)), c("x", "..."))
     xtable_args <- dots[intersect(names(dots), xtable_argnames)]
     non_xtable_args <- dots[setdiff(names(dots), xtable_argnames)]
 
-    # Call xtable with its args
+    # By default, numbers are right-aligned and everything else is left-aligned.
+    defaultAlignment <- function(col) {
+      if (is.numeric(col)) "r" else "l"
+    }
+
+    # Figure out column alignment
+    ## Case 1: default alignment
+    if (is.null(align) || align == "?") {
+      names <- defaultAlignment(attr(data, "row.names"))
+      cols <- paste(vapply(data, defaultAlignment, character(1)), collapse = "")
+      cols <- paste0(names, cols)
+    } else {
+      ## Case 2: user-specified alignment
+      num_cols <- if (rownames) nchar(align) else nchar(align)+1
+      valid <- !grepl("[^lcr\\?]", align)
+      if (num_cols == ncol(data)+1 && valid) {
+        cols <- if (rownames) align else paste0("r", align)
+        defaults <- grep("\\?", strsplit(cols,"")[[1]])
+        if (length(defaults) != 0) {
+          vals <- vapply(data[,defaults-1], defaultAlignment, character(1))
+          for (i in seq_len(length(defaults))) {
+            substr(cols, defaults[i], defaults[i]) <- vals[i]
+          }
+        }
+      } else if (nchar(align) == 1 && valid) {
+        cols <- paste0(rep(align, ncol(data)+1), collapse="")
+      } else {
+        stop("`align` must contain only the characters `l`, `c`, `r` and/or `?` and",
+             "have length either equal to 1 or to the total number of columns")
+      }
+    }
+
+    # Call xtable with its (updated) args
+    xtable_args <- c(xtable_args, align = cols, digits = digits)
     xtable_res <- do.call(xtable, c(list(data), xtable_args))
 
     # Set up print args
     print_args <- list(
-      xtable_res,
+      x = xtable_res,
       type = 'html',
-      html.table.attributes = paste('class="', htmlEscape(classNames, TRUE),
-                                    '"', sep='')
-    )
+      include.rownames = {
+        if ("include.rownames" %in% names(dots)) dots$include.rownames
+        else rownames
+      },
+      include.colnames = {
+        if ("include.colnames" %in% names(dots)) dots$include.colnames
+        else colnames
+      },
+      NA.string = {
+        if ("NA.string" %in% names(dots)) dots$NA.string
+        else na
+      },
+      html.table.attributes =
+        paste0({
+          if ("html.table.attributes" %in% names(dots)) dots$html.table.attributes
+          else ""
+        }, " ",
+        "class = '", htmlEscape(classNames, TRUE), "' ",
+        "style = 'width:", validateCssUnit(width), ";'"))
+
     print_args <- c(print_args, non_xtable_args)
+    print_args <- print_args[unique(names(print_args))]
+
+    # Capture the raw html table returned by print.xtable(), and store it in
+    # a variable for further processing
+    tab <- paste(utils::capture.output(do.call(print, print_args)),collapse = "\n")
+
+    # Add extra class to cells with NA value, to be able to style them separately
+    tab <- gsub(paste(">", na, "<"), paste(" class='NA'>", na, "<"), tab)
+
+    # All further processing concerns the table headers, so we don't need to run
+    # any of this if colnames=FALSE
+    if (colnames) {
+      # Make sure that the final html table has a proper header (not included
+      # in the print.xtable() default)
+      tab <- sub("<tr>", "<thead> <tr>", tab)
+      tab <- sub("</tr>", "</tr> </thead> <tbody>", tab)
+      tab <- sub("</table>$", "</tbody> </table>", tab)
+
+      # Update the `cols` string (which stores the alignment of each column) so
+      # that it only includes the alignment for the table variables (and not
+      # for the row.names)
+      cols <- if (rownames) cols else substr(cols, 2, nchar(cols))
+
+      # Create a vector whose i-th entry corresponds to the i-th table variable
+      # alignment (substituting "l" by "left", "c" by "center" and "r" by "right")
+      cols <- strsplit(cols, "")[[1]]
+      cols[cols == "l"] <- "left"
+      cols[cols == "r"] <- "right"
+      cols[cols == "c"] <- "center"
+
+      # Align each header accordingly (this guarantees that each header and its
+      # corresponding column have the same alignment)
+      for (i in seq_len(length(cols))) {
+        tab <- sub("<th>", paste0("<th style='text-align: ", cols[i], ";'>"), tab)
+      }
+    }
+    return(tab)
+  }
 
-    return(paste(
-      utils::capture.output(
-        do.call(print, print_args)
-      ),
-      collapse="\n"
-    ))
-  })
+  # Main render function
+  markRenderFunction(tableOutput, renderFunc, outputArgs = outputArgs)
 }
diff --git a/R/run-url.R b/R/run-url.R
index 8dbb838..f7d182e 100644
--- a/R/run-url.R
+++ b/R/run-url.R
@@ -22,7 +22,7 @@
 #' @export
 #' @examples
 #' ## Only run this example in interactive R sessions
-#'   if (interactive()) {
+#' if (interactive()) {
 #'   runUrl('https://github.com/rstudio/shiny_example/archive/master.tar.gz')
 #'
 #'   # Can run an app from a subdirectory in the archive
diff --git a/R/serializers.R b/R/serializers.R
new file mode 100644
index 0000000..1fdb8b0
--- /dev/null
+++ b/R/serializers.R
@@ -0,0 +1,72 @@
+# For most types of values, simply return the value unchanged.
+serializerDefault <- function(value, stateDir) {
+  value
+}
+
+
+serializerFileInput <- function(value, stateDir = NULL) {
+  # File inputs can be serialized only if there's a stateDir
+  if (is.null(stateDir)) {
+    return(serializerUnserializable())
+  }
+
+  # value is a data frame. When persisting files, we need to copy the file to
+  # the persistent dir and then strip the original path before saving.
+  newpaths <- file.path(stateDir, basename(value$datapath))
+  file.copy(value$datapath, newpaths, overwrite = TRUE)
+  value$datapath <- basename(newpaths)
+
+  value
+}
+
+
+# Return a sentinel value that represents "unserializable". This is applied to
+# for example, passwords and actionButtons.
+serializerUnserializable <- function(value, stateDir) {
+  structure(
+    list(),
+    serializable = FALSE
+  )
+}
+
+# Is this an "unserializable" sentinel value?
+isUnserializable <- function(x) {
+  identical(
+    attr(x, "serializable", exact = TRUE),
+    FALSE
+  )
+}
+
+
+# Given a reactiveValues object and optional directory for saving state, apply
+# serializer function to each of the values, and return a list of the returned
+# values. This function passes stateDir to the serializer functions, so if
+# stateDir is non-NULL, it can have a side effect of writing values to disk (in
+# stateDir).
+serializeReactiveValues <- function(values, exclude, stateDir = NULL) {
+  impl <- .subset2(values, "impl")
+
+  # Get named list where keys and values are the names of inputs; we'll retrieve
+  # actual values later.
+  vals <- isolate(impl$names())
+  vals <- setdiff(vals, exclude)
+  names(vals) <- vals
+
+  # Get values and apply serializer functions
+  vals <- lapply(vals, function(name) {
+    val <- impl$get(name)
+
+    # Get the serializer function for this input value. If none specified, use
+    # the default.
+    serializer <- impl$getMeta(name, "shiny.serializer")
+    if (is.null(serializer))
+      serializer <- serializerDefault
+
+    # Apply serializer function.
+    serializer(val, stateDir)
+  })
+
+  # Filter out any values that were marked as unserializable.
+  vals <- Filter(Negate(isUnserializable), vals)
+  vals
+}
diff --git a/R/server-input-handlers.R b/R/server-input-handlers.R
index 9c6bb74..0d7865b 100644
--- a/R/server-input-handlers.R
+++ b/R/server-input-handlers.R
@@ -71,6 +71,63 @@ removeInputHandler <- function(type){
   inputHandlers$remove(type)
 }
 
+
+# Apply input handler to a single input value
+applyInputHandler <- function(name, val, shinysession) {
+  splitName <- strsplit(name, ':')[[1]]
+  if (length(splitName) > 1) {
+    if (!inputHandlers$containsKey(splitName[[2]])) {
+      # No input handler registered for this type
+      stop("No handler registered for type ", name)
+    }
+
+    inputName <- splitName[[1]]
+
+    # Get the function for processing this type of input
+    inputHandler <- inputHandlers$get(splitName[[2]])
+
+    return(inputHandler(val, shinysession, inputName))
+
+  } else if (is.list(val) && is.null(names(val))) {
+    return(unlist(val, recursive = TRUE))
+  } else {
+    return(val)
+  }
+}
+
+#' Apply input handlers to raw input values
+#'
+#' The purpose of this function is to make it possible for external packages to
+#' test Shiny inputs. It takes a named list of raw input values, applies input
+#' handlers to those values, and then returns a named list of the processed
+#' values.
+#'
+#' The raw input values should be in a named list. Some values may have names
+#' like \code{"x:shiny.date"}. This function would apply the \code{"shiny.date"}
+#' input handler to the value, and then rename the result to \code{"x"}, in the
+#' output.
+#'
+#' @param inputs A named list of input values.
+#' @param shinysession A Shiny session object.
+#'
+#' @seealso registerInputHandler
+#' @keywords internal
+applyInputHandlers <- function(inputs, shinysession = getDefaultReactiveDomain()) {
+  inputs <- mapply(applyInputHandler, names(inputs), inputs,
+                   MoreArgs = list(shinysession = shinysession),
+                   SIMPLIFY = FALSE)
+
+  # Convert names like "button1:shiny.action" to "button1"
+  names(inputs) <- vapply(
+    names(inputs),
+    function(name) { strsplit(name, ":")[[1]][1] },
+    FUN.VALUE = character(1)
+  )
+
+  inputs
+}
+
+
 # Takes a list-of-lists and returns a matrix. The lists
 # must all be the same length. NULL is replaced by NA.
 registerInputHandler("shiny.matrix", function(data, ...) {
@@ -89,10 +146,29 @@ registerInputHandler("shiny.number", function(val, ...){
   ifelse(is.null(val), NA, val)
 })
 
+registerInputHandler("shiny.password", function(val, shinysession, name) {
+  # Mark passwords as not serializable
+  .subset2(shinysession$input, "impl")$setMeta(name, "shiny.serializer", serializerUnserializable)
+  val
+})
+
 registerInputHandler("shiny.date", function(val, ...){
   # First replace NULLs with NA, then convert to Date vector
   datelist <- ifelse(lapply(val, is.null), NA, val)
-  as.Date(unlist(datelist))
+
+  res <- NULL
+  tryCatch({
+      res <- as.Date(unlist(datelist))
+    },
+    error = function(e) {
+      # It's possible for client to send a string like "99999-01-01", which
+      # as.Date can't handle.
+      warning(e$message)
+      res <<- as.Date(rep(NA, length(datelist)))
+    }
+  )
+
+  res
 })
 
 registerInputHandler("shiny.datetime", function(val, ...){
@@ -104,8 +180,41 @@ registerInputHandler("shiny.datetime", function(val, ...){
   as.POSIXct(unlist(times), origin = "1970-01-01", tz = "UTC")
 })
 
-registerInputHandler("shiny.action", function(val, ...) {
+registerInputHandler("shiny.action", function(val, shinysession, name) {
   # mark up the action button value with a special class so we can recognize it later
   class(val) <- c(class(val), "shinyActionButtonValue")
   val
 })
+
+registerInputHandler("shiny.file", function(val, shinysession, name) {
+  # This function is only used when restoring a Shiny fileInput. When a file is
+  # uploaded the usual way, it takes a different code path and won't hit this
+  # function.
+  if (is.null(val))
+    return(NULL)
+
+  # The data will be a named list of lists; convert to a data frame.
+  val <- as.data.frame(lapply(val, unlist), stringsAsFactors = FALSE)
+
+  # `val$datapath` should be a filename without a path, for security reasons.
+  if (basename(val$datapath) != val$datapath) {
+    stop("Invalid '/' found in file input path.")
+  }
+
+  # Prepend the persistent dir
+  oldfile <- file.path(getCurrentRestoreContext()$dir, val$datapath)
+
+  # Copy the original file to a new temp dir, so that a restored session can't
+  # modify the original.
+  newdir <- file.path(tempdir(), createUniqueId(12))
+  dir.create(newdir)
+  val$datapath <- file.path(newdir, val$datapath)
+  file.copy(oldfile, val$datapath)
+
+  # Need to mark this input value with the correct serializer. When a file is
+  # uploaded the usual way (instead of being restored), this occurs in
+  # session$`@uploadEnd`.
+  .subset2(shinysession$input, "impl")$setMeta(name, "shiny.serializer", serializerFileInput)
+
+  val
+})
diff --git a/R/server.R b/R/server.R
index 7035cac..a341bde 100644
--- a/R/server.R
+++ b/R/server.R
@@ -49,7 +49,6 @@ registerClient <- function(client) {
 #'
 #' @examples
 #' addResourcePath('datasets', system.file('data', package='datasets'))
-#'
 #' @export
 addResourcePath <- function(prefix, directoryPath) {
   prefix <- prefix[1]
@@ -141,7 +140,6 @@ resourcePathHandler <- function(req) {
 #'   })
 #' }
 #' }
-#'
 #' @export
 shinyServer <- function(func) {
   .globals$server <- list(func)
@@ -220,7 +218,8 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
           if (is.character(msg))
             msg <- charToRaw(msg)
 
-          if (isTRUE(getOption('shiny.trace'))) {
+          traceOption <- getOption('shiny.trace', FALSE)
+          if (isTRUE(traceOption) || traceOption == "recv") {
             if (binary)
               message("RECV ", '$$binary data$$')
             else
@@ -232,121 +231,105 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
 
           msg <- decodeMessage(msg)
 
-          # Do our own list simplifying here. sapply/simplify2array give names to
-          # character vectors, which is rarely what we want.
-          if (!is.null(msg$data)) {
-            for (name in names(msg$data)) {
-              val <- msg$data[[name]]
-
-              splitName <- strsplit(name, ':')[[1]]
-              if (length(splitName) > 1) {
-                msg$data[[name]] <- NULL
+          # Set up a restore context from .clientdata_url_search before
+          # handling all the input values, because the restore context may be
+          # used by an input handler (like the one for "shiny.file"). This
+          # should only happen once, when the app starts.
+          if (is.null(shinysession$restoreContext)) {
+            bookmarkStore <- getShinyOption("bookmarkStore", default = "disable")
+            if (bookmarkStore == "disable") {
+              # If bookmarking is disabled, use empty context
+              shinysession$restoreContext <- RestoreContext$new()
+            } else {
+              # If there's bookmarked state, save it on the session object
+              shinysession$restoreContext <- RestoreContext$new(msg$data$.clientdata_url_search)
+            }
+          }
 
-                if (!inputHandlers$containsKey(splitName[[2]])){
-                  # No input handler registered for this type
-                  stop("No handler registered for for type ", name)
+          withRestoreContext(shinysession$restoreContext, {
+
+            msg$data <- applyInputHandlers(msg$data)
+
+            switch(
+              msg$method,
+              init = {
+
+                serverFunc <- withReactiveDomain(NULL, serverFuncSource())
+                if (!identicalFunctionBodies(serverFunc, appvars$server)) {
+                  appvars$server <- serverFunc
+                  if (!is.null(appvars$server))
+                  {
+                    # Tag this function as the Shiny server function. A debugger may use this
+                    # tag to give this function special treatment.
+                    # It's very important that it's appvars$server itself and NOT a copy that
+                    # is invoked, otherwise new breakpoints won't be picked up.
+                    attr(appvars$server, "shinyServerFunction") <- TRUE
+                    registerDebugHook("server", appvars, "Server Function")
+                  }
                 }
 
-                msg$data[[ splitName[[1]] ]] <-
-                  inputHandlers$get(splitName[[2]])(
-                    val,
-                    shinysession,
-                    splitName[[1]] )
-              }
-              else if (is.list(val) && is.null(names(val))) {
-                val_flat <- unlist(val, recursive = TRUE)
-
-                if (is.null(val_flat)) {
-                  # This is to assign NULL instead of deleting the item
-                  msg$data[name] <- list(NULL)
-                } else {
-                  msg$data[[name]] <- val_flat
+                # Check for switching into/out of showcase mode
+                if (.globals$showcaseOverride &&
+                    exists(".clientdata_url_search", where = msg$data)) {
+                  mode <- showcaseModeOfQuerystring(msg$data$.clientdata_url_search)
+                  if (!is.null(mode))
+                    shinysession$setShowcase(mode)
                 }
-              }
-            }
-          }
 
-          switch(
-            msg$method,
-            init = {
-
-              serverFunc <- withReactiveDomain(NULL, serverFuncSource())
-              if (!identicalFunctionBodies(serverFunc, appvars$server)) {
-                appvars$server <- serverFunc
-                if (!is.null(appvars$server))
-                {
-                  # Tag this function as the Shiny server function. A debugger may use this
-                  # tag to give this function special treatment.
-                  # It's very important that it's appvars$server itself and NOT a copy that
-                  # is invoked, otherwise new breakpoints won't be picked up.
-                  attr(appvars$server, "shinyServerFunction") <- TRUE
-                  registerDebugHook("server", appvars, "Server Function")
+                shinysession$manageInputs(msg$data)
+
+                # The client tells us what singletons were rendered into
+                # the initial page
+                if (!is.null(msg$data$.clientdata_singletons)) {
+                  shinysession$singletons <- strsplit(
+                    msg$data$.clientdata_singletons, ',')[[1]]
                 }
-              }
-
-              # Check for switching into/out of showcase mode
-              if (.globals$showcaseOverride &&
-                  exists(".clientdata_url_search", where = msg$data)) {
-                mode <- showcaseModeOfQuerystring(msg$data$.clientdata_url_search)
-                if (!is.null(mode))
-                  shinysession$setShowcase(mode)
-              }
-
-              shinysession$manageInputs(msg$data)
-
-              # The client tells us what singletons were rendered into
-              # the initial page
-              if (!is.null(msg$data$.clientdata_singletons)) {
-                shinysession$singletons <- strsplit(
-                  msg$data$.clientdata_singletons, ',')[[1]]
-              }
-
-              local({
-                args <- argsForServerFunc(serverFunc, shinysession)
-
-                withReactiveDomain(shinysession, {
-                  do.call(
-                    # No corresponding ..stacktraceoff; the server func is pure
-                    # user code
-                    wrapFunctionLabel(appvars$server, "server",
-                      ..stacktraceon = TRUE
-                    ),
-                    args
-                  )
+
+                local({
+                  args <- argsForServerFunc(serverFunc, shinysession)
+
+                  withReactiveDomain(shinysession, {
+                    do.call(
+                      # No corresponding ..stacktraceoff; the server func is pure
+                      # user code
+                      wrapFunctionLabel(appvars$server, "server",
+                        ..stacktraceon = TRUE
+                      ),
+                      args
+                    )
+                  })
                 })
-              })
-            },
-            update = {
-              shinysession$manageInputs(msg$data)
-            },
-            shinysession$dispatch(msg)
-          )
-          shinysession$manageHiddenOutputs()
-
-          if (exists(".shiny__stdout", globalenv()) &&
-              exists("HTTP_GUID", ws$request)) {
-            # safe to assume we're in shiny-server
-            shiny_stdout <- get(".shiny__stdout", globalenv())
-
-            # eNter a flushReact
-            writeLines(paste("_n_flushReact ", get("HTTP_GUID", ws$request),
-              " @ ", sprintf("%.3f", as.numeric(Sys.time())),
-              sep=""), con=shiny_stdout)
-            flush(shiny_stdout)
-
-            flushReact()
-
-            # eXit a flushReact
-            writeLines(paste("_x_flushReact ", get("HTTP_GUID", ws$request),
-              " @ ", sprintf("%.3f", as.numeric(Sys.time())),
-              sep=""), con=shiny_stdout)
-            flush(shiny_stdout)
-          } else {
-            flushReact()
-          }
-          lapply(appsByToken$values(), function(shinysession) {
-            shinysession$flushOutput()
-            NULL
+              },
+              update = {
+                shinysession$manageInputs(msg$data)
+              },
+              shinysession$dispatch(msg)
+            )
+            shinysession$manageHiddenOutputs()
+
+            if (exists(".shiny__stdout", globalenv()) &&
+                exists("HTTP_GUID", ws$request)) {
+              # safe to assume we're in shiny-server
+              shiny_stdout <- get(".shiny__stdout", globalenv())
+
+              # eNter a flushReact
+              writeLines(paste("_n_flushReact ", get("HTTP_GUID", ws$request),
+                " @ ", sprintf("%.3f", as.numeric(Sys.time())),
+                sep=""), con=shiny_stdout)
+              flush(shiny_stdout)
+
+              flushReact()
+
+              # eXit a flushReact
+              writeLines(paste("_x_flushReact ", get("HTTP_GUID", ws$request),
+                " @ ", sprintf("%.3f", as.numeric(Sys.time())),
+                sep=""), con=shiny_stdout)
+              flush(shiny_stdout)
+            } else {
+              flushReact()
+            }
+
+            flushAllSessions()
           })
         })
       }
@@ -447,6 +430,12 @@ startApp <- function(appObj, port, host, quiet) {
       message('\n', 'Listening on domain socket ', port)
     }
     mask <- attr(port, 'mask')
+    if (is.null(mask)) {
+      stop("`port` is not a valid domain socket (missing `mask` attribute). ",
+           "Note that if you're using the default `host` + `port` ",
+           "configuration (and not domain sockets), then `port` must ",
+           "be numeric, not a string.")
+    }
     return(startPipeServer(port, mask, handlerManager$createHttpuvApp()))
   }
 }
@@ -460,10 +449,7 @@ serviceApp <- function() {
     }
 
     flushReact()
-
-    for (shinysession in appsByToken$values()) {
-      shinysession$flushOutput()
-    }
+    flushAllSessions()
   }
 
   # If this R session is interactive, then call service() with a short timeout
@@ -565,17 +551,23 @@ runApp <- function(appDir=getwd(),
     handlerManager$clear()
   }, add = TRUE)
 
+  # Enable per-app Shiny options
+  oldOptionSet <- .globals$options
+  on.exit({
+    .globals$options <- oldOptionSet
+  },add = TRUE)
 
   if (is.null(host) || is.na(host))
     host <- '0.0.0.0'
 
   # Make warnings print immediately
-  ops <- options(warn = 1)
+  # Set pool.scheduler to support pool package
+  ops <- options(warn = 1, pool.scheduler = scheduleTask)
   on.exit(options(ops), add = TRUE)
 
   workerId(workerId)
 
-  if (nzchar(Sys.getenv('SHINY_PORT'))) {
+  if (inShinyServer()) {
     # If SHINY_PORT is set, we're running under Shiny Server. Check the version
     # to make sure it is compatible. Older versions of Shiny Server don't set
     # SHINY_SERVER_VERSION, those will return "" which is considered less than
@@ -607,21 +599,38 @@ runApp <- function(appDir=getwd(),
       on.exit(close(con), add = TRUE)
       settings <- read.dcf(con)
       if ("DisplayMode" %in% colnames(settings)) {
-        mode <- settings[1,"DisplayMode"]
+        mode <- settings[1, "DisplayMode"]
         if (mode == "Showcase") {
           setShowcaseDefault(1)
+          if ("IncludeWWW" %in% colnames(settings)) {
+            .globals$IncludeWWW <- as.logical(settings[1, "IncludeWWW"])
+            if (is.na(.globals$IncludeWWW)) {
+              stop("In your Description file, `IncludeWWW` ",
+                   "must be set to `True` (default) or `False`")
+            }
+          } else {
+            .globals$IncludeWWW <- TRUE
+          }
         }
       }
     }
   }
 
+  ## default is to show the .js, .css and .html files in the www directory
+  ## (if not in showcase mode, this variable will simply be ignored)
+  if (is.null(.globals$IncludeWWW) || is.na(.globals$IncludeWWW)) {
+    .globals$IncludeWWW <- TRUE
+  }
+
   # If display mode is specified as an argument, apply it (overriding the
   # value specified in DESCRIPTION, if any).
   display.mode <- match.arg(display.mode)
-  if (display.mode == "normal")
+  if (display.mode == "normal") {
     setShowcaseDefault(0)
-  else if (display.mode == "showcase")
+  }
+  else if (display.mode == "showcase") {
     setShowcaseDefault(1)
+  }
 
   require(shiny)
 
@@ -642,7 +651,14 @@ runApp <- function(appDir=getwd(),
       }
       else {
         # Try up to 20 random ports
-        port <- p_randomInt(3000, 8000)
+        while (TRUE) {
+          port <- p_randomInt(3000, 8000)
+          # Reject ports in this range that are considered unsafe by Chrome
+          # http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
+          if (!port %in% c(3659, 4045, 6000, 6665:6669)) {
+            break
+          }
+        }
       }
 
       # Test port to see if we can use it
@@ -656,6 +672,12 @@ runApp <- function(appDir=getwd(),
   }
 
   appParts <- as.shiny.appobj(appDir)
+
+  # Extract appOptions (which is a list) and store them as shinyOptions, for
+  # this app. (This is the only place we have to store settings that are
+  # accessible both the UI and server portion of the app.)
+  unconsumeAppOptions(appParts$appOptions)
+
   # Set up the onEnd before we call onStart, so that it gets called even if an
   # error happens in onStart.
   if (!is.null(appParts$onEnd))
@@ -718,7 +740,6 @@ runApp <- function(appDir=getwd(),
 #'
 #' @param returnValue The value that should be returned from
 #'   \code{\link{runApp}}.
-#'
 #' @export
 stopApp <- function(returnValue = invisible()) {
   # reterror will indicate whether retval is an error (i.e. it should be passed
@@ -837,7 +858,6 @@ runExample <- function(example=NA,
 #' # ...or as a single app object
 #' runGadget(shinyApp(ui, server))
 #' }
-#'
 #' @export
 runGadget <- function(app, server = NULL, port = getOption("shiny.port"),
   viewer = paneViewer(), stopOnCancel = TRUE) {
@@ -933,3 +953,9 @@ browserViewer <- function(browser = getOption("browser")) {
     utils::browseURL(url, browser = browser)
   }
 }
+
+# Returns TRUE if we're running in Shiny Server or other hosting environment,
+# otherwise returns FALSE.
+inShinyServer <- function() {
+  nzchar(Sys.getenv('SHINY_PORT'))
+}
diff --git a/R/shiny-options.R b/R/shiny-options.R
new file mode 100644
index 0000000..0a6907b
--- /dev/null
+++ b/R/shiny-options.R
@@ -0,0 +1,83 @@
+.globals$options <- list()
+
+#' @param name Name of an option to get.
+#' @param default Value to be returned if the option is not currently set.
+#' @rdname shinyOptions
+#' @export
+getShinyOption <- function(name, default = NULL) {
+  # Make sure to use named (not numeric) indexing
+  name <- as.character(name)
+
+  if (name %in% names(.globals$options))
+    .globals$options[[name]]
+  else
+    default
+}
+
+#' Get or set Shiny options
+#'
+#' \code{getShinyOption} retrieves the value of a Shiny option.
+#' \code{shinyOptions} sets the value of Shiny options; it can also be used to
+#' return a list of all currently-set Shiny options.
+#'
+#' There is a global option set, which is available by default. When a Shiny
+#' application is run with \code{\link{runApp}}, that option set is duplicated
+#' and the new option set is available for getting or setting values. If options
+#' are set from global.R, app.R, ui.R, or server.R, or if they are set from
+#' inside the server function, then the options will be scoped to the
+#' application. When the application exits, the new option set is discarded and
+#' the global option set is restored.
+#'
+#' @param ... Options to set, with the form \code{name = value}.
+#'
+#' @examples
+#' \dontrun{
+#' shinyOptions(myOption = 10)
+#' getShinyOption("myOption")
+#' }
+#' @export
+shinyOptions <- function(...) {
+  newOpts <- list(...)
+
+  if (length(newOpts) > 0) {
+    .globals$options <- dropNulls(mergeVectors(.globals$options, newOpts))
+    invisible(.globals$options)
+  } else {
+    .globals$options
+  }
+}
+
+
+# Eval an expression with a new option set
+withLocalOptions <- function(expr) {
+  oldOptionSet <- .globals$options
+  on.exit(.globals$options <- oldOptionSet)
+
+  expr
+}
+
+
+# Get specific shiny options and put them in a list, reset those shiny options,
+# and then return the options list. This should be during the creation of a
+# shiny app object, which happens before another option frame is added to the
+# options stack (the new option frame is added when the app is run). This
+# function "consumes" the options when the shinyApp object is created, so the
+# options won't affect another app that is created later.
+consumeAppOptions <- function() {
+  options <- list(
+    appDir = getwd(),
+    bookmarkStore = getShinyOption("bookmarkStore")
+  )
+
+  shinyOptions(appDir = NULL, bookmarkStore = NULL)
+
+  options
+}
+
+# Do the inverse of consumeAppOptions. This should be called once the app is
+# started.
+unconsumeAppOptions <- function(options) {
+  if (!is.null(options)) {
+    do.call(shinyOptions, options)
+  }
+}
diff --git a/R/shiny.R b/R/shiny.R
index 6fcab67..e6f49f9 100644
--- a/R/shiny.R
+++ b/R/shiny.R
@@ -39,9 +39,12 @@ NULL
 #'     when an app is run. See \code{\link{runApp}} for more information.}
 #'   \item{shiny.port}{A port number that Shiny will listen on. See
 #'     \code{\link{runApp}} for more information.}
-#'   \item{shiny.trace}{If \code{TRUE}, all of the messages sent between the R
-#'     server and the web browser client will be printed on the console. This
-#'     is useful for debugging.}
+#'   \item{shiny.trace}{Print messages sent between the R server and the web
+#'     browser client to the R console. This is useful for debugging. Possible
+#'     values are \code{"send"} (only print messages sent to the client),
+#'     \code{"recv"} (only print messages received by the server), \code{TRUE}
+#'     (print all messages), or \code{FALSE} (default; don't print any of these
+#'     messages).}
 #'   \item{shiny.autoreload}{If \code{TRUE} when a Shiny app is launched, the
 #'     app directory will be continually monitored for changes to files that
 #'     have the extensions: r, htm, html, js, css, png, jpg, jpeg, gif. If any
@@ -53,10 +56,10 @@ NULL
 #'
 #'     You can customize the file patterns Shiny will monitor by setting the
 #'     shiny.autoreload.pattern option. For example, to monitor only ui.R:
-#'     \code{option(shiny.autoreload.pattern = glob2rx("ui.R"))}
+#'     \code{options(shiny.autoreload.pattern = glob2rx("ui.R"))}
 #'
 #'     The default polling interval is 500 milliseconds. You can change this
-#'     by setting e.g. \code{option(shiny.autoreload.interval = 2000)} (every
+#'     by setting e.g. \code{options(shiny.autoreload.interval = 2000)} (every
 #'     two seconds).}
 #'   \item{shiny.reactlog}{If \code{TRUE}, enable logging of reactive events,
 #'     which can be viewed later with the \code{\link{showReactLog}} function.
@@ -96,6 +99,17 @@ NULL
 #'     an arguably more intuitive arrangement for casual R users, as the name
 #'     of a function appears next to the srcref where it is defined, rather than
 #'     where it is currently being called from.}
+#'   \item{shiny.sanitize.errors}{If \code{TRUE}, then normal errors (i.e.
+#'     errors not wrapped in \code{safeError}) won't show up in the app; a simple
+#'     generic error message is printed instead (the error and strack trace printed
+#'     to the console remain unchanged). The default is \code{FALSE} (unsanitized
+#'     errors).If you want to sanitize errors in general, but you DO want a
+#'     particular error \code{e} to get displayed to the user, then set this option
+#'     to \code{TRUE} and use \code{stop(safeError(e))} for errors you want the
+#'     user to see.}
+#'   \item{shiny.testmode}{If \code{TRUE}, then enable features for testing Shiny
+#'     applications. If \code{FALSE} (the default), do not enable those features.
+#'   }
 #' }
 #' @name shiny-options
 NULL
@@ -115,10 +129,14 @@ createUniqueId <- function(bytes, prefix = "", suffix = "") {
 toJSON <- function(x, ...,  dataframe = "columns", null = "null", na = "null",
   auto_unbox = TRUE, digits = getOption("shiny.json.digits", 16),
   use_signif = TRUE, force = TRUE, POSIXt = "ISO8601", UTC = TRUE,
-  rownames = FALSE, keep_vec_names = TRUE) {
+  rownames = FALSE, keep_vec_names = TRUE, strict_atomic = TRUE) {
+
+  if (strict_atomic) {
+    x <- I(x)
+  }
 
   # I(x) is so that length-1 atomic vectors get put in [].
-  jsonlite::toJSON(I(x), dataframe = dataframe, null = null, na = na,
+  jsonlite::toJSON(x, dataframe = dataframe, null = null, na = na,
    auto_unbox = auto_unbox, digits = digits, use_signif = use_signif,
    force = force, POSIXt = POSIXt, UTC = UTC, rownames = rownames,
    keep_vec_names = keep_vec_names, json_verbatim = TRUE, ...)
@@ -160,6 +178,18 @@ workerId <- local({
 #' example, \code{session$clientData$url_search}).
 #'
 #' @return
+#' \item{allowReconnect(value)}{
+#'   If \code{value} is \code{TRUE} and run in a hosting environment (Shiny
+#'   Server or Connect) with reconnections enabled,  then when the session ends
+#'   due to the network connection closing, the client will attempt to
+#'   reconnect to the server. If a reconnection is successful, the browser will
+#'   send all the current input values to the new session on the server, and
+#'   the server will recalculate any outputs and send them back to the client.
+#'   If \code{value} is \code{FALSE}, reconnections will be disabled (this is
+#'   the default state). If \code{"force"}, then the client browser will always
+#'   attempt to reconnect. The only reason to use \code{"force"} is for testing
+#'   on a local connection (without Shiny Server or Connect).
+#' }
 #' \item{clientData}{
 #'   A \code{\link{reactiveValues}} object that contains information about the client.
 #'   \itemize{
@@ -194,6 +224,11 @@ workerId <- local({
 #' \item{isClosed()}{A function that returns \code{TRUE} if the client has
 #'   disconnected.
 #' }
+#' \item{ns(id)}{
+#'   Server-side version of \code{ns <- \link{NS}(id)}. If bare IDs need to be
+#'   explicitly namespaced for the current module, \code{session$ns("name")}
+#'   will return the fully-qualified ID.
+#' }
 #' \item{onEnded(callback)}{
 #'   Synonym for \code{onSessionEnded}.
 #' }
@@ -241,6 +276,10 @@ workerId <- local({
 #'   This is the request that was used to initiate the websocket connection
 #'   (as opposed to the request that downloaded the web page for the app).
 #' }
+#' \item{resetBrush(brushId)}{
+#'   Resets/clears the brush with the given \code{brushId}, if it exists on
+#'   any \code{imageOutput} or \code{plotOutput} in the app.
+#' }
 #' \item{sendCustomMessage(type, message)}{
 #'   Sends a custom message to the web page. \code{type} must be a
 #'   single-element character vector giving the type of message, while
@@ -253,6 +292,12 @@ workerId <- local({
 #'   \code{addCustomMessageHandler} will be invoked each time
 #'   \code{sendCustomMessage} is called on the server.
 #' }
+#' \item{sendBinaryMessage(type, message)}{
+#'   Similar to \code{sendCustomMessage}, but the message must be a raw vector
+#'   and the registration method on the client is
+#'   \code{Shiny.addBinaryMessageHandler(type, function(message){...})}. The
+#'   message argument on the client will be a \href{https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView}{DataView}.
+#' }
 #' \item{sendInputMessage(inputId, message)}{
 #'   Sends a message to an input on the session's client web page; if the input
 #'   is present and bound on the page at the time the message is received, then
@@ -261,10 +306,41 @@ workerId <- local({
 #'   from Shiny apps, but through friendlier wrapper functions like
 #'   \code{\link{updateTextInput}}.
 #' }
-#' \item{ns(id)}{
-#'   Server-side version of \code{ns <- \link{NS}(id)}. If bare IDs need to be
-#'   explicitly namespaced for the current module, \code{session$ns("name")}
-#'   will return the fully-qualified ID.
+#' \item{setBookmarkExclude(names)}{
+#'   Set input names to be excluded from bookmarking.
+#' }
+#' \item{getBookmarkExclude()}{
+#'   Returns the set of input names to be excluded from bookmarking.
+#' }
+#' \item{onBookmark(fun)}{
+#'   Registers a function that will be called just before bookmarking state.
+#' }
+#' \item{onBookmarked(fun)}{
+#'   Registers a function that will be called just after bookmarking state.
+#' }
+#' \item{onRestore(fun)}{
+#'   Registers a function that will be called when a session is restored, before
+#'   all other reactives, observers, and render functions are run.
+#' }
+#' \item{onRestored(fun)}{
+#'   Registers a function that will be called when a session is restored, after
+#'   all other reactives, observers, and render functions are run.
+#' }
+#' \item{doBookmark()}{
+#'   Do bookmarking and invoke the onBookmark and onBookmarked callback functions.
+#' }
+#' \item{exportTestValues()}{
+#'   Registers expressions for export in test mode, available at the test
+#'   endpoint URL.
+#' }
+#' \item{getTestEndpointUrl(inputs=TRUE, outputs=TRUE, exports=TRUE,
+#'   format="rds")}{
+#'   Returns a URL for the test endpoint. Only has an effect when the
+#'   \code{shiny.testmode} option is set to TRUE. For the inputs, outputs, and
+#'   exports arguments, TRUE means to return all of these values. It is also
+#'   possible to specify by name which values to return by providing a
+#'   character vector, as in \code{inputs=c("x", "y")}. The format can be
+#'   "rds" or "json".
 #' }
 #'
 #' @name session
@@ -292,7 +368,6 @@ NULL
 #' @return If \code{id} is missing, returns a function that expects an id string
 #'   as its only argument and returns that id with the namespace prepended.
 #' @seealso \url{http://shiny.rstudio.com/articles/modules.html}
-#'
 #' @export
 NS <- function(namespace, id = NULL) {
   if (missing(id)) {
@@ -308,6 +383,7 @@ NS <- function(namespace, id = NULL) {
 #' @export
 ns.sep <- "-"
 
+
 #' @include utils.R
 ShinySession <- R6Class(
   'ShinySession',
@@ -321,7 +397,7 @@ ShinySession <- R6Class(
     .outputs = list(),          # Keeps track of all the output observer objects
     .outputOptions = list(),     # Options for each of the output observer objects
     progressKeys = 'character',
-    showcase   = 'ANY',
+    showcase   = FALSE,
     fileUploadContext = 'FileUploadContext',
     .input      = 'ANY', # Internal ReactiveValues object for normal input sent from client
     .clientData = 'ANY', # Internal ReactiveValues object for other data sent from the client
@@ -330,28 +406,51 @@ ShinySession <- R6Class(
     flushCallbacks = 'Callbacks',
     flushedCallbacks = 'Callbacks',
     inputReceivedCallbacks = 'Callbacks',
+    bookmarkCallbacks = 'Callbacks',
+    bookmarkedCallbacks = 'Callbacks',
+    restoreCallbacks = 'Callbacks',
+    restoredCallbacks = 'Callbacks',
+    bookmarkExclude = character(0),  # Names of inputs to exclude from bookmarking
+
+    testValueExprs = list(),
+    outputValues = list(),           # Saved output values (for testing mode)
+    testEndpointUrl = character(0),
+
     sendResponse = function(requestMsg, value) {
       if (is.null(requestMsg$tag)) {
         warning("Tried to send response for untagged message; method: ",
                 requestMsg$method)
         return()
       }
-      private$write(toJSON(list(response=list(tag=requestMsg$tag, value=value))))
+      private$sendMessage(
+        response = list(tag = requestMsg$tag, value = value)
+      )
     },
     sendErrorResponse = function(requestMsg, error) {
       if (is.null(requestMsg$tag))
         return()
-      private$write(toJSON(list(response=list(tag=requestMsg$tag, error=error))))
+      private$sendMessage(
+        response = list(tag = requestMsg$tag, error = error)
+      )
     },
     write = function(json) {
       if (self$closed){
         return()
       }
-      if (isTRUE(getOption('shiny.trace')))
+      traceOption <- getOption('shiny.trace', FALSE)
+      if (isTRUE(traceOption) || traceOption == "send")
         message('SEND ',
            gsub('(?m)base64,[a-zA-Z0-9+/=]+','[base64 data]',json,perl=TRUE))
       private$websocket$send(json)
     },
+    sendMessage = function(...) {
+      # This function is a wrapper for $write
+      msg <- list(...)
+      if (anyUnnamed(msg)) {
+        stop("All arguments to sendMessage must be named.")
+      }
+      private$write(toJSON(msg))
+    },
     getOutputOption = function(outputName, propertyName, defaultValue) {
       opts <- private$.outputOptions[[outputName]]
       if (is.null(opts))
@@ -384,9 +483,191 @@ ShinySession <- R6Class(
 
       # Clear file upload directories, if present
       self$onSessionEnded(private$fileUploadContext$rmUploadDirs)
+    },
+
+    createBookmarkObservers = function() {
+      # This is to be called from the initialization. It registers observers
+      # for bookmarking to work.
+
+      # Get bookmarking config
+      store <- getShinyOption("bookmarkStore", default = "disable")
+      if (store == "disable")
+        return()
+
+      # Warn if trying to enable save-to-server bookmarking on a version of SS,
+      # SSP, or Connect that doesn't support it.
+      if (store == "server" && inShinyServer() &&
+          is.null(getShinyOption("save.interface")))
+      {
+        showNotification(
+          "This app tried to enable saved-to-server bookmarking, but it is not supported by the hosting environment.",
+          duration = NULL, type = "warning", session = self
+        )
+        return()
+      }
+
+      withReactiveDomain(self, {
+        # This observer fires when the bookmark button is clicked.
+        observeEvent(self$input[["._bookmark_"]], {
+          self$doBookmark()
+        })
+
+        # If there was an error initializing the current restore context, show
+        # notification in the client.
+        observe({
+          rc <- getCurrentRestoreContext()
+          if (!is.null(rc$initErrorMessage)) {
+            showNotification(
+              paste("Error in RestoreContext initialization:", rc$initErrorMessage),
+              duration = NULL, type = "error"
+            )
+          }
+        })
+
+        # Run the onRestore function at the beginning of the flush cycle, but after
+        # the server function has been executed.
+        observe({
+          if (private$restoreCallbacks$count() > 0) {
+            tryCatch(
+              withLogErrors(
+                isolate({
+                  rc <- getCurrentRestoreContext()
+                  if (rc$active) {
+                    restoreState <- getCurrentRestoreContext()$asList()
+                    private$restoreCallbacks$invoke(restoreState)
+                  }
+                })
+              ),
+              error = function(e) {
+                showNotification(
+                  paste0("Error calling onRestore callback: ", e$message),
+                  duration = NULL, type = "error"
+                )
+              }
+            )
+          }
+        }, priority = 1000000)
+
+        # Run the onRestored function after the flush cycle completes and information
+        # is sent to the client.
+        self$onFlushed(function() {
+          if (private$restoredCallbacks$count() > 0) {
+
+            tryCatch(
+              withLogErrors(
+                isolate({
+                  rc <- getCurrentRestoreContext()
+                  if (rc$active) {
+                    restoreState <- getCurrentRestoreContext()$asList()
+                    private$restoredCallbacks$invoke(restoreState)
+                  }
+                })
+              ),
+              error = function(e) {
+                msg <- paste0("Error calling onRestored callback: ", e$message)
+                showNotification(msg, duration = NULL, type = "error")
+              }
+            )
+          }
+        })
+
+      }) # withReactiveDomain
+    },
+
+    # Save output values and errors. This is only used for testing mode.
+    storeOutputValues = function(values = NULL) {
+      private$outputValues <- mergeVectors(private$outputValues, values)
+    },
+
+    enableTestEndpoint = function() {
+      private$testEndpointUrl <- self$registerDataObj("shinytest", NULL,
+        function(data, req) {
+          if (!isTRUE(getOption("shiny.testmode"))) {
+            return()
+          }
+
+          params <- parseQueryString(req$QUERY_STRING)
+          # The format of the response that will be sent back. Default to "rds"
+          # unless requested otherwise. The only other valid value is "json".
+          format <- params$format %OR% "rds"
+
+          values <- list()
+
+          if (!is.null(params$inputs)) {
+
+            allInputs <- isolate(
+              reactiveValuesToList(self$input, all.names = TRUE)
+            )
+
+            # If params$inputs is "1", return all; otherwise return just the
+            # inputs that are named in params$inputs, like "x,y,z".
+            if (params$inputs == "1") {
+              values$inputs <- allInputs
+            } else {
+              items <- strsplit(params$inputs, ",")[[1]]
+              items <- intersect(items, names(allInputs))
+              values$inputs <- allInputs[items]
+            }
+          }
+
+          if (!is.null(params$outputs)) {
+
+            if (params$outputs == "1") {
+              values$outputs <- private$outputValues
+            } else {
+              items <- strsplit(params$outputs, ",")[[1]]
+              items <- intersect(items, names(private$outputValues))
+              values$outputs <- private$outputValues[items]
+            }
+          }
+
+          if (!is.null(params$exports)) {
+
+            testValueExprs <- private$testValueExprs
+            if (params$exports == "1") {
+              values$exports <- isolate(
+                lapply(private$testValueExprs, function(item) {
+                  eval(item$expr, envir = item$env)
+                })
+              )
+            } else {
+              items <- strsplit(params$exports, ",")[[1]]
+              items <- intersect(items, names(private$testValueExprs))
+              values$exports <- isolate(
+                lapply(private$testValueExprs[items], function(item) {
+                  eval(item$expr, envir = item$env)
+                })
+              )
+            }
+          }
+
+          if (length(values) == 0) {
+            return(httpResponse(400, "text/plain",
+              "No exports, inputs, or outputs requested."
+            ))
+          }
+
+          if (identical(format, "json")) {
+            content <- toJSON(values, pretty = TRUE)
+            httpResponse(200, "application/json", content)
+
+          } else if (identical(format, "rds")) {
+            tmpfile <- tempfile("shinytest", fileext = ".rds")
+            saveRDS(values, tmpfile)
+            on.exit(unlink(tmpfile))
+
+            content <- readBin(tmpfile, "raw", n = file.info(tmpfile)$size)
+            httpResponse(200, "application/octet-stream", content)
+
+          } else {
+            httpResponse(400, "text/plain", paste("Invalid format requested:", format))
+          }
+        }
+      )
     }
   ),
   public = list(
+    restoreContext = NULL,
     progressStack = 'Stack', # Stack of progress objects
     input       = 'reactivevalues', # Externally-usable S3 wrapper object for .input
     output      = 'ANY',    # Externally-usable S3 wrapper object for .outputs
@@ -429,6 +710,14 @@ ShinySession <- R6Class(
       private$.outputs <- list()
       private$.outputOptions <- list()
 
+      private$bookmarkCallbacks <- Callbacks$new()
+      private$bookmarkedCallbacks <- Callbacks$new()
+      private$restoreCallbacks <- Callbacks$new()
+      private$restoredCallbacks <- Callbacks$new()
+      private$createBookmarkObservers()
+
+      private$enableTestEndpoint()
+
       private$registerSessionEndCallbacks()
 
       if (!is.null(websocket$request$HTTP_SHINY_SERVER_CREDENTIALS)) {
@@ -444,15 +733,27 @@ ShinySession <- R6Class(
       # tries to access session$request
       delayedAssign('request', websocket$request, assign.env = self)
 
-      private$write(toJSON(list(config = list(
-        workerId = workerId(),
-        sessionId = self$token
-      ))))
+      private$sendMessage(
+        config = list(
+          workerId = workerId(),
+          sessionId = self$token
+        )
+      )
+    },
+    rootScope = function() {
+      self
     },
     makeScope = function(namespace) {
       ns <- NS(namespace)
 
-      createSessionProxy(self,
+      # Private items for this scope. Can't be part of the scope object because
+      # `$<-.session_proxy` doesn't allow assignment on overidden names.
+      bookmarkCallbacks <- Callbacks$new()
+      restoreCallbacks  <- Callbacks$new()
+      restoredCallbacks <- Callbacks$new()
+      bookmarkExclude   <- character(0)
+
+      scope <- createSessionProxy(self,
         input = .createReactiveValues(private$.input, readonly = TRUE, ns = ns),
         output = .createOutputWriter(self, ns = ns),
         sendInputMessage = function(inputId, message) {
@@ -464,12 +765,175 @@ ShinySession <- R6Class(
         ns = ns,
         makeScope = function(namespace) {
           self$makeScope(ns(namespace))
+        },
+
+        setBookmarkExclude = function(names) {
+          bookmarkExclude <<- names
+        },
+        getBookmarkExclude = function() {
+          bookmarkExclude
+        },
+        onBookmark = function(fun) {
+          if (!is.function(fun) || length(fun) != 1) {
+            stop("`fun` must be a function that takes one argument")
+          }
+          bookmarkCallbacks$register(fun)
+        },
+        onBookmarked = function(fun) {
+          stop("onBookmarked() can't be used in a module.")
+        },
+        onRestore = function(fun) {
+          if (!is.function(fun) || length(fun) != 1) {
+            stop("`fun` must be a function that takes one argument")
+          }
+          restoreCallbacks$register(fun)
+        },
+        onRestored = function(fun) {
+          if (!is.function(fun) || length(fun) != 1) {
+            stop("`fun` must be a function that takes one argument")
+          }
+          restoredCallbacks$register(fun)
+        },
+        exportTestValues = function(..., quoted_ = FALSE, env_ = parent.frame()) {
+          if (quoted_) {
+            dots <- list(...)
+          } else {
+            dots <- eval(substitute(alist(...)))
+          }
+
+          if (anyUnnamed(dots))
+            stop("exportTestValues: all arguments must be named.")
+
+          names(dots) <- vapply(names(dots), ns, character(1))
+
+          do.call(
+            .subset2(self, "exportTestValues"),
+            c(dots, quoted_ = TRUE, env_ = env_),
+            quote = TRUE
+          )
         }
       )
+
+      # Given a char vector, return a logical vector indicating which of those
+      # strings are names of things in the namespace.
+      filterNamespace <- function(x) {
+        nsString <- paste0(namespace, ns.sep)
+        substr(x, 1, nchar(nsString)) == nsString
+      }
+
+      # Given a char vector of namespaced names, return a char vector of corresponding
+      # names with namespace prefix removed.
+      unNamespace <- function(x) {
+        if (!all(filterNamespace(x))) {
+          stop("x contains strings(s) that do not have namespace prefix ", namespace)
+        }
+
+        nsString <- paste0(namespace, ns.sep)
+        substring(x, nchar(nsString) + 1)
+      }
+
+      # Given a restore state object (a list), return a modified version that's
+      # scoped to this namespace.
+      scopeRestoreState <- function(state) {
+        # State is a list. We need to copy and transform some things for the
+        # scope.
+        scopeState <- state
+        # `values` is an environment and we don't want to modify the original.
+        scopeState$values <- new.env(parent = emptyenv())
+
+        # Keep only inputs that are in the scope, and rename them
+        scopeState$input <- scopeState$input[filterNamespace(names(scopeState$input))]
+        names(scopeState$input) <- unNamespace(names(scopeState$input))
+
+        # Same for values. This is an environment so we have to handle a little
+        # differently.
+        origNames <- names(state$values)
+        origNames <- origNames[filterNamespace(origNames)]
+        lapply(origNames, function(origName) {
+          scopedName <- unNamespace(origName)
+          scopeState$values[[scopedName]] <- state$values[[origName]]
+        })
+
+        if (!is.null(state$dir)) {
+          dir <- file.path(state$dir, namespace)
+          if (dirExists(dir))
+            scopeState$dir <- dir
+        }
+
+        scopeState
+      }
+
+      # When scope is created, register these bookmarking callbacks on the main
+      # session object. They will invoke the scope's own callbacks, if any are
+      # present.
+      self$onBookmark(function(state) {
+        # Exit if no user-defined callbacks.
+        if (bookmarkCallbacks$count() == 0)
+          return()
+
+        scopeState <- ShinySaveState$new(scope$input, scope$getBookmarkExclude())
+
+        # Create subdir for this scope
+        if (!is.null(state$dir)) {
+          scopeState$dir <- file.path(state$dir, namespace)
+          res <- dir.create(scopeState$dir)
+          if (res == FALSE) {
+            stop("Error creating subdirectory for scope ", namespace)
+          }
+        }
+
+        # Invoke the callback on the scopeState object
+        bookmarkCallbacks$invoke(scopeState)
+
+        # Copy `values` from scopeState to state, adding namespace
+        if (length(scopeState$values) != 0) {
+          if (anyUnnamed(scopeState$values)) {
+            stop("All scope values in must be named.")
+          }
+
+          lapply(names(scopeState$values), function(origName) {
+            scopedName <- ns(origName)
+            state$values[[scopedName]] <- scopeState$values[[origName]]
+          })
+        }
+      })
+
+      self$onRestore(function(state) {
+        # Exit if no user-defined callbacks.
+        if (restoreCallbacks$count() == 0)
+          return()
+
+        scopeState <- scopeRestoreState(state)
+        # Invoke user callbacks
+        restoreCallbacks$invoke(scopeState)
+      })
+
+      self$onRestored(function(state) {
+        # Exit if no user-defined callbacks.
+        if (restoredCallbacks$count() == 0)
+          return()
+
+        scopeState <- scopeRestoreState(state)
+        # Invoke user callbacks
+        restoredCallbacks$invoke(scopeState)
+      })
+
+      scope
     },
     ns = function(id) {
       NS(NULL, id)
     },
+
+    # Freeze a value until the flush cycle completes
+    freezeValue = function(x, name) {
+      if (!is.reactivevalues(x))
+        stop("x must be a reactivevalues object")
+
+      impl <- .subset2(x, 'impl')
+      impl$freeze(name)
+      self$onFlushed(function() impl$thaw(name))
+    },
+
     onSessionEnded = function(sessionEndedCallback) {
       "Registers the given callback to be invoked when the session is closed
       (i.e. the connection to the client has been severed). The return value
@@ -502,10 +966,7 @@ ShinySession <- R6Class(
       # ..stacktraceon matches with the top-level ..stacktraceoff..
       private$closedCallbacks$invoke(onError = printError, ..stacktraceon = TRUE)
       flushReact()
-      lapply(appsByToken$values(), function(shinysession) {
-        shinysession$flushOutput()
-        NULL
-      })
+      flushAllSessions()
     },
     isClosed = function() {
       return(self$closed)
@@ -516,6 +977,14 @@ ShinySession <- R6Class(
     setShowcase = function(value) {
       private$showcase <- !is.null(value) && as.logical(value)
     },
+
+    allowReconnect = function(value) {
+      if (!(identical(value, TRUE) || identical(value, FALSE) || identical(value, "force"))) {
+        stop('value must be TRUE, FALSE, or "force"')
+      }
+      private$write(toJSON(list(allowReconnect = value)))
+    },
+
     defineOutput = function(name, func, label) {
       "Binds an output generating function to this name. The function can either
       take no parameters, or have named parameters for \\code{name} and
@@ -553,35 +1022,45 @@ ShinySession <- R6Class(
 
         obs <- observe(..stacktraceon = FALSE, {
 
-          self$sendCustomMessage('recalculating', list(
+          private$sendMessage(recalculating = list(
             name = name, status = 'recalculating'
           ))
 
           value <- tryCatch(
             shinyCallingHandlers(func()),
+            shiny.custom.error = function(cond) {
+              if (isTRUE(getOption("show.error.messages"))) printError(cond)
+              structure(NULL, class = "try-error", condition = cond)
+            },
+            shiny.output.cancel = function(cond) {
+              structure(NULL, class = "cancel-output")
+            },
             shiny.silent.error = function(cond) {
               # Don't let shiny.silent.error go through the normal stop
               # path of try, because we don't want it to print. But we
               # do want to try to return the same looking result so that
               # the code below can send the error to the browser.
-              structure(
-                NULL,
-                class = "try-error",
-                condition = cond
-              )
+              structure(NULL, class = "try-error", condition = cond)
             },
             error = function(cond) {
-              msg <- paste0("Error in output$", name, ": ", conditionMessage(cond), "\n")
-              if (isTRUE(getOption("show.error.messages"))) {
-                printError(cond)
+              if (isTRUE(getOption("show.error.messages"))) printError(cond)
+              if (getOption("shiny.sanitize.errors", FALSE)) {
+                cond <- simpleError(paste("An error has occurred. Check your",
+                                          "logs or contact the app author for",
+                                          "clarification."))
               }
-              invisible(structure(msg, class = "try-error", condition = cond))
+              invisible(structure(NULL, class = "try-error", condition = cond))
+            },
+            finally = {
+              private$sendMessage(recalculating = list(
+                name = name, status = 'recalculated'
+              ))
             }
           )
 
-          self$sendCustomMessage('recalculating', list(
-            name = name, status = 'recalculated'
-          ))
+          if (inherits(value, "cancel-output")) {
+            return()
+          }
 
           private$invalidatedOutputErrors$remove(name)
           private$invalidatedOutputValues$remove(name)
@@ -612,33 +1091,59 @@ ShinySession <- R6Class(
       }
     },
     flushOutput = function() {
+      if (self$isClosed())
+        return()
+
+      # Return TRUE if there's any stuff to send to the client.
+      hasPendingUpdates <- function() {
+        # Even though progressKeys isn't sent to the client, we use it in this
+        # check. This is because if it is non-empty, sending `values` to the
+        # client tells it that the flushReact loop is finished, and the client
+        # then knows to stop showing progress.
+        return(
+          length(private$progressKeys) != 0 ||
+          length(private$invalidatedOutputValues) != 0 ||
+          length(private$invalidatedOutputErrors) != 0 ||
+          length(private$inputMessageQueue) != 0
+        )
+      }
 
       # ..stacktraceon matches with the top-level ..stacktraceoff..
       private$flushCallbacks$invoke(..stacktraceon = TRUE)
-      # ..stacktraceon matches with the top-level ..stacktraceoff..
-      on.exit(private$flushedCallbacks$invoke(..stacktraceon = TRUE))
 
-      if (length(private$progressKeys) == 0
-          && length(private$invalidatedOutputValues) == 0
-          && length(private$invalidatedOutputErrors) == 0
-          && length(private$inputMessageQueue) == 0) {
+      # Schedule execution of onFlushed callbacks
+      on.exit({
+        # ..stacktraceon matches with the top-level ..stacktraceoff..
+        private$flushedCallbacks$invoke(..stacktraceon = TRUE)
+
+        # If one of the flushedCallbacks added anything to send to the client,
+        # or invalidated any observers, set up another flush cycle.
+        if (hasPendingUpdates() || .getReactiveEnvironment()$hasPendingFlush()) {
+          scheduleFlush()
+        }
+      })
+
+      if (!hasPendingUpdates()) {
         return(invisible())
       }
 
       private$progressKeys <- character(0)
-
-      values <- private$invalidatedOutputValues
+      values <- as.list(private$invalidatedOutputValues)
       private$invalidatedOutputValues <- Map$new()
-      errors <- private$invalidatedOutputErrors
+      errors <- as.list(private$invalidatedOutputErrors)
       private$invalidatedOutputErrors <- Map$new()
       inputMessages <- private$inputMessageQueue
       private$inputMessageQueue <- list()
 
-      json <- toJSON(list(errors=as.list(errors),
-                          values=as.list(values),
-                          inputMessages=inputMessages))
+      if (isTRUE(getOption("shiny.testmode"))) {
+        private$storeOutputValues(mergeVectors(values, errors))
+      }
 
-      private$write(json)
+      private$sendMessage(
+        errors = errors,
+        values = values,
+        inputMessages = inputMessages
+      )
     },
     showProgress = function(id) {
       'Send a message to the client that recalculation of the output identified
@@ -659,16 +1164,23 @@ ShinySession <- R6Class(
       self$sendProgress('binding', list(id = id))
     },
     sendProgress = function(type, message) {
-      json <- toJSON(list(
+      private$sendMessage(
         progress = list(type = type, message = message)
-      ))
-      private$write(json)
+      )
+    },
+    sendNotification = function(type, message) {
+      private$sendMessage(
+        notification = list(type = type, message = message)
+      )
+    },
+    sendModal = function(type, message) {
+      private$sendMessage(
+        modal = list(type = type, message = message)
+      )
     },
     dispatch = function(msg) {
       method <- paste('@', msg$method, sep='')
-      # we must use $ instead of [[ here at the moment; see
-      # https://github.com/rstudio/shiny/issues/274
-      func <- try(do.call(`$`, list(self, method)), silent=TRUE)
+      func <- try(self[[method]], silent = TRUE)
       if (inherits(func, 'try-error')) {
         private$sendErrorResponse(msg, paste('Unknown method', msg$method))
       }
@@ -682,10 +1194,17 @@ ShinySession <- R6Class(
         private$sendResponse(msg, value)
       }
     },
+    sendBinaryMessage = function(type, message) {
+      typeBytes <- charToRaw(type)
+      if (length(typeBytes) > 255) {
+        stop("'type' argument is too long")
+      }
+      private$write(c(as.raw(length(typeBytes)), typeBytes, message))
+    },
     sendCustomMessage = function(type, message) {
       data <- list()
       data[[type]] <- message
-      private$write(toJSON(list(custom=data)))
+      private$sendMessage(custom = data)
     },
     sendInputMessage = function(inputId, message) {
       data <- list(id = inputId, message = message)
@@ -715,12 +1234,162 @@ ShinySession <- R6Class(
         return(dereg)
       }
     },
+
+    setBookmarkExclude = function(names) {
+      private$bookmarkExclude <- names
+    },
+    getBookmarkExclude = function() {
+      private$bookmarkExclude
+    },
+    onBookmark = function(fun) {
+      if (!is.function(fun) || length(fun) != 1) {
+        stop("`fun` must be a function that takes one argument")
+      }
+      private$bookmarkCallbacks$register(fun)
+    },
+    onBookmarked = function(fun) {
+      if (!is.function(fun) || length(fun) != 1) {
+        stop("`fun` must be a function that takes one argument")
+      }
+      private$bookmarkedCallbacks$register(fun)
+    },
+    onRestore = function(fun) {
+      if (!is.function(fun) || length(fun) != 1) {
+        stop("`fun` must be a function that takes one argument")
+      }
+      private$restoreCallbacks$register(fun)
+    },
+    onRestored = function(fun) {
+      if (!is.function(fun) || length(fun) != 1) {
+        stop("`fun` must be a function that takes one argument")
+      }
+      private$restoredCallbacks$register(fun)
+    },
+    doBookmark = function() {
+      # Get bookmarking store config
+      store <- getShinyOption("bookmarkStore", default = "disable")
+      if (store == "disable")
+        return()
+
+      tryCatch(
+        withLogErrors({
+          saveState <- ShinySaveState$new(
+            input = self$input,
+            exclude = self$getBookmarkExclude(),
+            onSave = function(state) {
+              private$bookmarkCallbacks$invoke(state)
+            }
+          )
+
+          if (store == "server") {
+            url <- saveShinySaveState(saveState)
+          } else if (store == "url") {
+            url <- encodeShinySaveState(saveState)
+          } else {
+            stop("Unknown store type: ", store)
+          }
+
+          clientData <- self$clientData
+          url <- paste0(
+            clientData$url_protocol, "//",
+            clientData$url_hostname,
+            if (nzchar(clientData$url_port)) paste0(":", clientData$url_port),
+            clientData$url_pathname,
+            "?", url
+          )
+
+
+          # If onBookmarked callback was provided, invoke it; if not call
+          # the default.
+          if (private$bookmarkedCallbacks$count() > 0) {
+            private$bookmarkedCallbacks$invoke(url)
+          } else {
+            showBookmarkUrlModal(url)
+          }
+        }),
+        error = function(e) {
+          msg <- paste0("Error bookmarking state: ", e$message)
+          showNotification(msg, duration = NULL, type = "error")
+        }
+      )
+    },
+
+    exportTestValues = function(..., quoted_ = FALSE, env_ = parent.frame()) {
+      # Get a named list of unevaluated expressions.
+      if (quoted_) {
+        dots <- list(...)
+      } else {
+        dots <- eval(substitute(alist(...)))
+      }
+
+      if (anyUnnamed(dots))
+        stop("exportTestValues: all arguments must be named.")
+
+      # Create a named list where each item is a list with an expression and
+      # environment in which to eval the expression.
+      items <- lapply(dots, function(expr) {
+        list(expr = expr, env = env_)
+      })
+
+      private$testValueExprs <- mergeVectors(private$testValueExprs, items)
+    },
+
+    getTestEndpointUrl = function(inputs = TRUE, outputs = TRUE, exports = TRUE,
+                                  format = "rds") {
+      reqString <- function(group, value) {
+        if (isTRUE(value))
+          paste0(group, "=1")
+        else if (is.character(value))
+          paste0(group, "=", paste(value, collapse = ","))
+        else
+          ""
+      }
+      paste(
+        private$testEndpointUrl,
+        reqString("inputs", inputs),
+        reqString("outputs", outputs),
+        reqString("exports", exports),
+        paste0("format=", format),
+        sep = "&"
+      )
+    },
+
     reactlog = function(logEntry) {
+      # Use sendCustomMessage instead of sendMessage, because the handler in
+      # shiny-showcase.js only has access to public API of the Shiny object.
       if (private$showcase)
         self$sendCustomMessage("reactlog", logEntry)
     },
     reload = function() {
-      self$sendCustomMessage("reload", TRUE)
+      private$sendMessage(reload = TRUE)
+    },
+    sendInsertUI = function(selector, multiple, where, content) {
+      private$sendMessage(
+        `shiny-insert-ui` = list(
+          selector = selector,
+          multiple = multiple,
+          where = where,
+          content = content
+        )
+      )
+    },
+    sendRemoveUI = function(selector, multiple) {
+      private$sendMessage(
+        `shiny-remove-ui` = list(
+          selector = selector,
+          multiple = multiple
+        )
+      )
+    },
+    updateQueryString = function(queryString) {
+      private$sendMessage(updateQueryString = list(queryString = queryString))
+    },
+    resetBrush = function(brushId) {
+      private$sendMessage(
+        resetBrush = list(
+          brushId = brushId
+        )
+      )
     },
 
     # Public RPC methods
@@ -748,6 +1417,9 @@ ShinySession <- R6Class(
     `@uploadEnd` = function(jobId, inputId) {
       fileData <- private$fileUploadContext$getUploadOperation(jobId)$finish()
       private$.input$set(inputId, fileData)
+
+      private$.input$setMeta(inputId, "shiny.serializer", serializerFileInput)
+
       invisible()
     },
     # Provides a mechanism for handling direct HTTP requests that are posted
@@ -854,13 +1526,14 @@ ShinySession <- R6Class(
         # ..stacktraceon matches with the top-level ..stacktraceoff..
         result <- try(shinyCallingHandlers(Context$new(getDefaultReactiveDomain(), '[download]')$run(
           function() { ..stacktraceon..(download$func(tmpdata)) }
-        )))
+        )), silent = TRUE)
         if (inherits(result, 'try-error')) {
-          cond <- attr(result, 'condition', exact = TRUE)
-          printError(cond)
           unlink(tmpdata)
-          return(httpResponse(500, 'text/plain; charset=UTF-8',
-                              enc2utf8(conditionMessage(cond))))
+          stop(attr(result, "condition", exact = TRUE))
+        }
+        if (!file.exists(tmpdata)) {
+          # If no file was created, return a 404
+          return(httpResponse(404, content = "404 Not found"))
         }
         return(httpResponse(
           200,
@@ -932,10 +1605,11 @@ ShinySession <- R6Class(
     registerDataObj = function(name, data, filterFunc) {
       # abusing downloads at the moment
       self$downloads$set(name, list(data = data, filter = filterFunc))
-      return(sprintf('session/%s/dataobj/%s?w=%s',
+      return(sprintf('session/%s/dataobj/%s?w=%s&nonce=%s',
                      URLencode(self$token, TRUE),
                      URLencode(name, TRUE),
-                     workerId()))
+                     workerId(),
+                     URLencode(createUniqueId(8), TRUE)))
     },
     # This function suspends observers for hidden outputs and resumes observers
     # for un-hidden outputs.
@@ -1002,14 +1676,14 @@ ShinySession <- R6Class(
     },
     incrementBusyCount = function() {
       if (private$busyCount == 0L) {
-        self$sendCustomMessage("busy", "busy")
+        private$sendMessage(busy = "busy")
       }
       private$busyCount <- private$busyCount + 1L
     },
     decrementBusyCount = function() {
       private$busyCount <- private$busyCount - 1L
       if (private$busyCount == 0L) {
-        self$sendCustomMessage("busy", "idle")
+        private$sendMessage(busy = "idle")
       }
     }
   ),
@@ -1101,10 +1775,72 @@ ShinySession <- R6Class(
 #' @param ... Options to set for the output observer.
 #' @export
 outputOptions <- function(x, name, ...) {
-  if (!inherits(x, "shinyoutput"))
+  if (!inherits(x, "shinyoutput")) {
     stop("x must be a shinyoutput object.")
+  }
 
-  name <- .subset2(x, 'ns')(name)
+  if (!missing(name)) {
+    name <- .subset2(x, 'ns')(name)
+  } else {
+    name <- NULL
+  }
 
   .subset2(x, 'impl')$outputOptions(name, ...)
 }
+
+
+#' Add callbacks for Shiny session events
+#'
+#' These functions are for registering callbacks on Shiny session events.
+#' \code{onFlush} registers a function that will be called before Shiny flushes
+#' the reactive system. \code{onFlushed} registers a function that will be
+#' called after Shiny flushes the reactive system. \code{onSessionEnded}
+#' registers a function to be called after the client has disconnected.
+#'
+#' These functions should be called within the application's server function.
+#'
+#' All of these functions return a function which can be called with no
+#' arguments to cancel the registration.
+#'
+#' @param fun A callback function.
+#' @param once Should the function be run once, and then cleared, or should it
+#'   re-run each time the event occurs. (Only for \code{onFlush} and
+#'   \code{onFlushed}.)
+#' @param session A shiny session object.
+#'
+#' @export
+onFlush <- function(fun, once = TRUE, session = getDefaultReactiveDomain()) {
+  session$onFlush(fun, once = once)
+}
+
+#' @rdname onFlush
+#' @export
+onFlushed <- function(fun, once = TRUE, session = getDefaultReactiveDomain()) {
+  session$onFlushed(fun, once = once)
+}
+
+#' @rdname onFlush
+#' @export
+onSessionEnded <- function(fun, session = getDefaultReactiveDomain()) {
+  session$onSessionEnded(fun)
+}
+
+
+scheduleFlush <- function() {
+  timerCallbacks$schedule(0, function() {})
+}
+
+flushAllSessions <- function() {
+  lapply(appsByToken$values(), function(shinysession) {
+    tryCatch(
+      shinysession$flushOutput(),
+
+      stop = function(e) {
+        # If there are any uncaught errors that bubbled up to here, close the
+        # session.
+        shinysession$close()
+      }
+    )
+    NULL
+  })
+}
diff --git a/R/shinyui.R b/R/shinyui.R
index 4dc2b34..e803c47 100644
--- a/R/shinyui.R
+++ b/R/shinyui.R
@@ -20,7 +20,7 @@ withMathJax <- function(...) {
       singleton(tags$script(src = path, type = 'text/javascript'))
     ),
     ...,
-    tags$script(HTML('MathJax.Hub.Queue(["Typeset", MathJax.Hub]);'))
+    tags$script(HTML('if (window.MathJax) MathJax.Hub.Queue(["Typeset", MathJax.Hub]);'))
   )
 }
 
@@ -44,7 +44,8 @@ renderPage <- function(ui, connection, showcase=0) {
 
   shiny_deps <- list(
     htmlDependency("json2", "2014.02.04", c(href="shared"), script = "json2-min.js"),
-    htmlDependency("jquery", "1.11.3", c(href="shared"), script = "jquery.min.js"),
+    htmlDependency("jquery", "1.12.4", c(href="shared"), script = "jquery.min.js"),
+    htmlDependency("babel-polyfill", "6.7.2", c(href="shared"), script = "babel-polyfill.min.js"),
     htmlDependency("shiny", utils::packageVersion("shiny"), c(href="shared"),
       script = if (getOption("shiny.minified", TRUE)) "shiny.min.js" else "shiny.js",
       stylesheet = "shiny.css")
@@ -63,7 +64,6 @@ renderPage <- function(ui, connection, showcase=0) {
 #'
 #' @param ui A user interace definition
 #' @return The user interface definition, without modifications or side effects.
-#'
 #' @export
 shinyUI <- function(ui) {
   .globals$ui <- list(ui)
@@ -90,17 +90,34 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
       if (!is.null(mode))
         showcaseMode <- mode
     }
-    uiValue <- if (is.function(ui)) {
-      if (length(formals(ui)) > 0) {
-        # No corresponding ..stacktraceoff.., this is pure user code
-        ..stacktraceon..(ui(req))
-      } else {
-        # No corresponding ..stacktraceoff.., this is pure user code
-        ..stacktraceon..(ui())
-      }
+
+    # Create a restore context using query string
+    bookmarkStore <- getShinyOption("bookmarkStore", default = "disable")
+    if (bookmarkStore == "disable") {
+      # If bookmarking is disabled, use empty context
+      restoreContext <- RestoreContext$new()
     } else {
-      ui
+      restoreContext <- RestoreContext$new(req$QUERY_STRING)
     }
+
+    withRestoreContext(restoreContext, {
+      uiValue <- NULL
+
+      if (is.function(ui)) {
+        if (length(formals(ui)) > 0) {
+          # No corresponding ..stacktraceoff.., this is pure user code
+          uiValue <- ..stacktraceon..(ui(req))
+        } else {
+          # No corresponding ..stacktraceoff.., this is pure user code
+          uiValue <- ..stacktraceon..(ui())
+        }
+      } else {
+        if (getCurrentRestoreContext()$active) {
+          warning("Trying to restore saved app state, but UI code must be a function for this to work! See ?enableBookmarking")
+        }
+        uiValue <- ui
+      }
+    })
     if (is.null(uiValue))
       return(NULL)
 
diff --git a/R/shinywrappers.R b/R/shinywrappers.R
index 0fa0ba7..3559aa0 100644
--- a/R/shinywrappers.R
+++ b/R/shinywrappers.R
@@ -12,24 +12,74 @@ globalVariables('func')
 #'   an output ID.
 #' @param renderFunc A function that is suitable for assigning to a Shiny output
 #'   slot.
+#' @param outputArgs A list of arguments to pass to the \code{uiFunc}. Render
+#'   functions should include \code{outputArgs = list()} in their own parameter
+#'   list, and pass through the value to \code{markRenderFunction}, to allow
+#'   app authors to customize outputs. (Currently, this is only supported for
+#'   dynamically generated UIs, such as those created by Shiny code snippets
+#'   embedded in R Markdown documents).
 #' @return The \code{renderFunc} function, with annotations.
-#'
 #' @export
-markRenderFunction <- function(uiFunc, renderFunc) {
+markRenderFunction <- function(uiFunc, renderFunc, outputArgs = list()) {
+  # a mutable object that keeps track of whether `useRenderFunction` has been
+  # executed (this usually only happens when rendering Shiny code snippets in
+  # an interactive R Markdown document); its initial value is FALSE
+  hasExecuted <- Mutable$new()
+  hasExecuted$set(FALSE)
+
+  origRenderFunc <- renderFunc
+  renderFunc <- function(...) {
+    # if the user provided something through `outputArgs` BUT the
+    # `useRenderFunction` was not executed, then outputArgs will be ignored,
+    # so throw a warning to let user know the correct usage
+    if (length(outputArgs) != 0 && !hasExecuted$get()) {
+      warning("Unused argument: outputArgs. The argument outputArgs is only ",
+              "meant to be used when embedding snippets of Shiny code in an ",
+              "R Markdown code chunk (using runtime: shiny). When running a ",
+              "full Shiny app, please set the output arguments directly in ",
+              "the corresponding output function of your UI code.")
+      # stop warning from happening again for the same object
+      hasExecuted$set(TRUE)
+    }
+    if (is.null(formals(origRenderFunc))) origRenderFunc()
+    else origRenderFunc(...)
+  }
+
   structure(renderFunc,
-            class      = c("shiny.render.function", "function"),
-            outputFunc = uiFunc)
+            class       = c("shiny.render.function", "function"),
+            outputFunc  = uiFunc,
+            outputArgs  = outputArgs,
+            hasExecuted = hasExecuted)
 }
 
 useRenderFunction <- function(renderFunc, inline = FALSE) {
   outputFunction <- attr(renderFunc, "outputFunc")
+  outputArgs <- attr(renderFunc, "outputArgs")
+  hasExecuted <- attr(renderFunc, "hasExecuted")
+  hasExecuted$set(TRUE)
+
+  for (arg in names(outputArgs)) {
+    if (!arg %in% names(formals(outputFunction))) {
+      stop(paste0("Unused argument: in 'outputArgs', '",
+                  arg, "' is not an valid argument for ",
+                  "the output function"))
+      outputArgs[[arg]] <- NULL
+    }
+  }
+
   id <- createUniqueId(8, "out")
+  # Make the id the first positional argument
+  outputArgs <- c(list(id), outputArgs)
+
   o <- getDefaultReactiveDomain()$output
   if (!is.null(o))
     o[[id]] <- renderFunc
-  if (is.logical(formals(outputFunction)[["inline"]])) {
-    outputFunction(id, inline = inline)
-  } else outputFunction(id)
+
+  if (is.logical(formals(outputFunction)[["inline"]]) && !("inline" %in% names(outputArgs))) {
+    outputArgs[["inline"]] <- inline
+  }
+
+  do.call(outputFunction, outputArgs)
 }
 
 #' @export
@@ -69,13 +119,23 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
 #'   it is sent to the client browser? Generally speaking, if the image is a
 #'   temp file generated within \code{func}, then this should be \code{TRUE};
 #'   if the image is not a temp file, this should be \code{FALSE}.
-#'
+#' @param outputArgs A list of arguments to be passed through to the implicit
+#'   call to \code{\link{imageOutput}} when \code{renderImage} is used in an
+#'   interactive R Markdown document.
 #' @export
 #'
 #' @examples
-#' \dontrun{
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   sliderInput("n", "Number of observations", 2, 1000, 500),
+#'   plotOutput("plot1"),
+#'   plotOutput("plot2"),
+#'   plotOutput("plot3")
+#' )
 #'
-#' shinyServer(function(input, output, clientData) {
+#' server <- function(input, output, session) {
 #'
 #'   # A plot of fixed size
 #'   output$plot1 <- renderImage({
@@ -97,14 +157,14 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
 #'   output$plot2 <- renderImage({
 #'     # Read plot2's width and height. These are reactive values, so this
 #'     # expression will re-run whenever these values change.
-#'     width  <- clientData$output_plot2_width
-#'     height <- clientData$output_plot2_height
+#'     width  <- session$clientData$output_plot2_width
+#'     height <- session$clientData$output_plot2_height
 #'
 #'     # A temp file to save the output.
 #'     outfile <- tempfile(fileext='.png')
 #'
 #'     png(outfile, width=width, height=height)
-#'     hist(rnorm(input$obs))
+#'     hist(rnorm(input$n))
 #'     dev.off()
 #'
 #'     # Return a list containing the filename
@@ -115,6 +175,8 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
 #'   }, deleteFile = TRUE)
 #'
 #'   # Send a pre-rendered image, and don't delete the image after sending it
+#'   # NOTE: For this example to work, it would require files in a subdirectory
+#'   # named images/
 #'   output$plot3 <- renderImage({
 #'     # When input$n is 1, filename is ./images/image1.jpeg
 #'     filename <- normalizePath(file.path('./images',
@@ -123,14 +185,15 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
 #'     # Return a list containing the filename
 #'     list(src = filename)
 #'   }, deleteFile = FALSE)
-#' })
+#' }
 #'
+#' shinyApp(ui, server)
 #' }
 renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
-                        deleteFile=TRUE) {
+                        deleteFile=TRUE, outputArgs=list()) {
   installExprFunction(expr, "func", env, quoted)
 
-  return(markRenderFunction(imageOutput, function(shinysession, name, ...) {
+  renderFunc <- function(shinysession, name, ...) {
     imageinfo <- func()
     # Should the file be deleted after being sent? If .deleteFile not set or if
     # TRUE, then delete; otherwise don't delete.
@@ -147,7 +210,9 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
     # Return a list with src, and other img attributes
     c(src = shinysession$fileUrl(name, file=imageinfo$src, contentType=contentType),
       extra_attr)
-  }))
+  }
+
+  markRenderFunction(imageOutput, renderFunc, outputArgs = outputArgs)
 }
 
 
@@ -173,28 +238,27 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
 #'   object.
 #' @param env The environment in which to evaluate \code{expr}.
 #' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This
-#' @param func A function that may print output and/or return a printable R
-#'   object (deprecated; use \code{expr} instead).
+#'   is useful if you want to save an expression in a variable.
 #' @param width The value for \code{\link{options}('width')}.
+#' @param outputArgs A list of arguments to be passed through to the implicit
+#'   call to \code{\link{verbatimTextOutput}} when \code{renderPrint} is used
+#'   in an interactive R Markdown document.
 #' @seealso \code{\link{renderText}} for displaying the value returned from a
 #'   function, instead of the printed output.
 #'
 #' @example res/text-example.R
-#'
 #' @export
-renderPrint <- function(expr, env = parent.frame(), quoted = FALSE, func = NULL,
-                        width = getOption('width')) {
-  if (!is.null(func)) {
-    shinyDeprecated(msg="renderPrint: argument 'func' is deprecated. Please use 'expr' instead.")
-  } else {
-    installExprFunction(expr, "func", env, quoted)
-  }
+renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
+                        width = getOption('width'), outputArgs=list()) {
+  installExprFunction(expr, "func", env, quoted)
 
-  markRenderFunction(verbatimTextOutput, function() {
+  renderFunc <- function(shinysession, name, ...) {
     op <- options(width = width)
     on.exit(options(op), add = TRUE)
     paste(utils::capture.output(func()), collapse = "\n")
-  })
+  }
+
+  markRenderFunction(verbatimTextOutput, renderFunc, outputArgs = outputArgs)
 }
 
 #' Text Output
@@ -215,26 +279,25 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE, func = NULL,
 #' @param env The environment in which to evaluate \code{expr}.
 #' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This
 #'   is useful if you want to save an expression in a variable.
-#' @param func A function that returns an R object that can be used as an
-#'   argument to \code{cat}.(deprecated; use \code{expr} instead).
+#' @param outputArgs A list of arguments to be passed through to the implicit
+#'   call to \code{\link{textOutput}} when \code{renderText} is used in an
+#'   interactive R Markdown document.
 #'
 #' @seealso \code{\link{renderPrint}} for capturing the print output of a
 #'   function, rather than the returned text value.
 #'
 #' @example res/text-example.R
-#'
 #' @export
-renderText <- function(expr, env=parent.frame(), quoted=FALSE, func=NULL) {
-  if (!is.null(func)) {
-    shinyDeprecated(msg="renderText: argument 'func' is deprecated. Please use 'expr' instead.")
-  } else {
-    installExprFunction(expr, "func", env, quoted)
-  }
+renderText <- function(expr, env=parent.frame(), quoted=FALSE,
+                       outputArgs=list()) {
+  installExprFunction(expr, "func", env, quoted)
 
-  markRenderFunction(textOutput, function() {
+  renderFunc <- function(shinysession, name, ...) {
     value <- func()
     return(paste(utils::capture.output(cat(value)), collapse="\n"))
-  })
+  }
+
+  markRenderFunction(textOutput, renderFunc, outputArgs = outputArgs)
 }
 
 #' UI Output
@@ -250,46 +313,44 @@ renderText <- function(expr, env=parent.frame(), quoted=FALSE, func=NULL) {
 #' @param env The environment in which to evaluate \code{expr}.
 #' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This
 #'   is useful if you want to save an expression in a variable.
-#' @param func A function that returns a Shiny tag object, \code{\link{HTML}},
-#'   or a list of such objects (deprecated; use \code{expr} instead).
+#' @param outputArgs A list of arguments to be passed through to the implicit
+#'   call to \code{\link{uiOutput}} when \code{renderUI} is used in an
+#'   interactive R Markdown document.
 #'
 #' @seealso conditionalPanel
-#'
 #' @export
 #' @examples
-#' \dontrun{
-#'   output$moreControls <- renderUI({
-#'     list(
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
 #'
+#' ui <- fluidPage(
+#'   uiOutput("moreControls")
+#' )
+#'
+#' server <- function(input, output) {
+#'   output$moreControls <- renderUI({
+#'     tagList(
+#'       sliderInput("n", "N", 1, 1000, 500),
+#'       textInput("label", "Label")
 #'     )
 #'   })
 #' }
-renderUI <- function(expr, env=parent.frame(), quoted=FALSE, func=NULL) {
-  if (!is.null(func)) {
-    shinyDeprecated(msg="renderUI: argument 'func' is deprecated. Please use 'expr' instead.")
-  } else {
-    installExprFunction(expr, "func", env, quoted)
-  }
+#' shinyApp(ui, server)
+#' }
+#'
+renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
+                     outputArgs=list()) {
+  installExprFunction(expr, "func", env, quoted)
 
-  markRenderFunction(uiOutput, function(shinysession, name, ...) {
+  renderFunc <- function(shinysession, name, ...) {
     result <- func()
     if (is.null(result) || length(result) == 0)
       return(NULL)
 
-    result <- takeSingletons(result, shinysession$singletons, desingleton=FALSE)$ui
-    result <- surroundSingletons(result)
-    dependencies <- lapply(resolveDependencies(findDependencies(result)),
-      createWebDependency)
-    names(dependencies) <- NULL
-
-    # renderTags returns a list with head, singletons, and html
-    output <- list(
-      html = doRenderTags(result),
-      deps = dependencies
-    )
+    processDeps(result, shinysession)
+  }
 
-    return(output)
-  })
+  markRenderFunction(uiOutput, renderFunc, outputArgs = outputArgs)
 }
 
 #' File Downloads
@@ -315,28 +376,40 @@ renderUI <- function(expr, env=parent.frame(), quoted=FALSE, func=NULL) {
 #'   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.
+#' @param outputArgs A list of arguments to be passed through to the implicit
+#'   call to \code{\link{downloadButton}} when \code{downloadHandler} is used
+#'   in an interactive R Markdown document.
 #'
 #' @examples
-#' \dontrun{
-#' # In server.R:
-#' output$downloadData <- downloadHandler(
-#'   filename = function() {
-#'     paste('data-', Sys.Date(), '.csv', sep='')
-#'   },
-#'   content = function(file) {
-#'     write.csv(data, file)
-#'   }
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   downloadLink("downloadData", "Download")
 #' )
 #'
-#' # In ui.R:
-#' downloadLink('downloadData', 'Download')
+#' server <- function(input, output) {
+#'   # Our dataset
+#'   data <- mtcars
+#'
+#'   output$downloadData <- downloadHandler(
+#'     filename = function() {
+#'       paste("data-", Sys.Date(), ".csv", sep="")
+#'     },
+#'     content = function(file) {
+#'       write.csv(data, file)
+#'     }
+#'   )
 #' }
 #'
+#' shinyApp(ui, server)
+#' }
 #' @export
-downloadHandler <- function(filename, content, contentType=NA) {
-  return(markRenderFunction(downloadButton, function(shinysession, name, ...) {
+downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()) {
+  renderFunc <- function(shinysession, name, ...) {
     shinysession$registerDownload(name, filename, contentType, content)
-  }))
+  }
+  markRenderFunction(downloadButton, renderFunc, outputArgs = outputArgs)
 }
 
 #' Table output with the JavaScript library DataTables
@@ -367,6 +440,10 @@ downloadHandler <- function(filename, content, contentType=NA) {
 #'   indicate which columns to escape, e.g. \code{1:5} (the first 5 columns),
 #'   \code{c(1, 3, 4)}, or \code{c(-1, -3)} (all columns except the first and
 #'   third), or \code{c('Species', 'Sepal.Length')}.
+#' @param outputArgs A list of arguments to be passed through to the implicit
+#'   call to \code{\link{dataTableOutput}} when \code{renderDataTable} is used
+#'   in an interactive R Markdown document.
+#'
 #' @references \url{http://datatables.net}
 #' @note This function only provides the server-side version of DataTables
 #'   (using R to process the data object on the server side). There is a
@@ -401,10 +478,11 @@ downloadHandler <- function(filename, content, contentType=NA) {
 #' }
 renderDataTable <- function(expr, options = NULL, searchDelay = 500,
                             callback = 'function(oTable) {}', escape = TRUE,
-                            env = parent.frame(), quoted = FALSE) {
+                            env = parent.frame(), quoted = FALSE,
+                            outputArgs=list()) {
   installExprFunction(expr, "func", env, quoted)
 
-  markRenderFunction(dataTableOutput, function(shinysession, name, ...) {
+  renderFunc <- function(shinysession, name, ...) {
     if (is.function(options)) options <- options()
     options <- checkDT9(options)
     res <- checkAsIs(options)
@@ -430,7 +508,9 @@ renderDataTable <- function(expr, options = NULL, searchDelay = 500,
       evalOptions = if (length(res$eval)) I(res$eval), searchDelay = searchDelay,
       callback = paste(callback, collapse = '\n'), escape = escape
     )
-  })
+  }
+
+  markRenderFunction(dataTableOutput, renderFunc, outputArgs = outputArgs)
 }
 
 # a data frame containing the DataTables 1.9 and 1.10 names
diff --git a/R/showcase.R b/R/showcase.R
index ca9b89f..fcdd0e9 100644
--- a/R/showcase.R
+++ b/R/showcase.R
@@ -31,7 +31,7 @@ licenseLink <- function(licenseName) {
 showcaseHead <- function() {
 
   deps  <- list(
-    htmlDependency("jqueryui", "1.11.4", c(href="shared/jqueryui"),
+    htmlDependency("jqueryui", "1.12.1", c(href="shared/jqueryui"),
       script = "jquery-ui.min.js"),
     htmlDependency("showdown", "0.3.1", c(href="shared/showdown/compressed"),
       script = "showdown.js"),
@@ -77,10 +77,60 @@ appMetadata <- function(desc) {
   else ""
 }
 
+navTabsHelper <- function(files, prefix = "") {
+  lapply(files, function(file) {
+    with(tags,
+      li(class=if (tolower(file) %in% c("app.r", "server.r")) "active" else "",
+         a(href=paste("#", gsub(".", "_", file, fixed=TRUE), "_code", sep=""),
+           "data-toggle"="tab", paste0(prefix, file)))
+    )
+  })
+}
+
+navTabsDropdown <- function(files) {
+  if (length(files) > 0) {
+    with(tags,
+      li(role="presentation", class="dropdown",
+        a(class="dropdown-toggle", `data-toggle`="dropdown", href="#",
+          role="button", `aria-haspopup`="true", `aria-expanded`="false",
+          "www", span(class="caret")
+        ),
+        ul(class="dropdown-menu", navTabsHelper(files))
+      )
+    )
+  }
+}
+
+tabContentHelper <- function(files, path, language) {
+  lapply(files, function(file) {
+    with(tags,
+      div(class=paste("tab-pane",
+                      if (tolower(file) %in% c("app.r", "server.r")) " active"
+                      else "",
+                      sep=""),
+          id=paste(gsub(".", "_", file, fixed=TRUE),
+                   "_code", sep=""),
+          pre(class="shiny-code",
+              # we need to prevent the indentation of <code> ... </code>
+              HTML(format(tags$code(
+                class=paste0("language-", language),
+                paste(readUTF8(file.path.ci(path, file)), collapse="\n")
+              ), indent = FALSE))))
+    )
+  })
+}
+
 # Returns tags containing the application's code in Bootstrap-style tabs in
 # showcase mode.
 showcaseCodeTabs <- function(codeLicense) {
   rFiles <- list.files(pattern = "\\.[rR]$")
+  wwwFiles <- list()
+  if (isTRUE(.globals$IncludeWWW)) {
+    path <- file.path(getwd(), "www")
+    wwwFiles$jsFiles <- list.files(path, pattern = "\\.js$")
+    wwwFiles$cssFiles <- list.files(path, pattern = "\\.css$")
+    wwwFiles$htmlFiles <- list.files(path, pattern = "\\.html$")
+  }
   with(tags, div(id="showcase-code-tabs",
     a(id="showcase-code-position-toggle",
       class="btn btn-default btn-sm",
@@ -88,27 +138,21 @@ showcaseCodeTabs <- function(codeLicense) {
       icon("level-up"),
       "show with app"),
     ul(class="nav nav-tabs",
-       lapply(rFiles, function(rFile) {
-         li(class=if (tolower(rFile) %in% c("app.r", "server.r")) "active" else "",
-            a(href=paste("#", gsub(".", "_", rFile, fixed=TRUE),
-                         "_code", sep=""),
-              "data-toggle"="tab", rFile))
-       })),
+       navTabsHelper(rFiles),
+       navTabsDropdown(unlist(wwwFiles))
+    ),
     div(class="tab-content", id="showcase-code-content",
-        lapply(rFiles, function(rFile) {
-          div(class=paste("tab-pane",
-                          if (tolower(rFile) %in% c("app.r", "server.r")) " active"
-                          else "",
-                          sep=""),
-              id=paste(gsub(".", "_", rFile, fixed=TRUE),
-                       "_code", sep=""),
-              pre(class="shiny-code",
-                  # we need to prevent the indentation of <code> ... </code>
-                  HTML(format(tags$code(
-                    class="language-r",
-                    paste(readUTF8(file.path.ci(getwd(), rFile)), collapse="\n")
-                  ), indent = FALSE))))
-        })),
+        tabContentHelper(rFiles, path = getwd(), language = "r"),
+        tabContentHelper(wwwFiles$jsFiles,
+                         path = paste0(getwd(), "/www"),
+                         language = "javascript"),
+        tabContentHelper(wwwFiles$cssFiles,
+                         path = paste0(getwd(), "/www"),
+                         language = "css"),
+        tabContentHelper(wwwFiles$htmlFiles,
+                         path = paste0(getwd(), "/www"),
+                         language = "xml")
+    ),
     codeLicense))
 }
 
@@ -177,3 +221,4 @@ showcaseUI <- function(ui) {
     showcaseBody(ui)
   )
 }
+
diff --git a/R/test-export.R b/R/test-export.R
new file mode 100644
index 0000000..a9b3b68
--- /dev/null
+++ b/R/test-export.R
@@ -0,0 +1,60 @@
+#' Register expressions for export in test mode
+#'
+#' This function registers expressions that will be evaluated when a test export
+#' event occurs. These events are triggered by accessing an API endpoint URL.
+#'
+#' This function only has an effect if the global option \code{shiny.testmode}
+#' is set to \code{TRUE}.
+#'
+#' @param quoted_ Are the expression quoted? Default is \code{FALSE}.
+#' @param env_ The environment in which the expression should be evaluated.
+#' @param session_ A Shiny session object.
+#' @param ... Named arguments that are quoted or unquoted expressions that will
+#'   be captured and evaluated when API endpoint is visited.
+#' @examples
+#' ## Only run this example in interactive R sessions
+#' if (interactive()) {
+#'
+#' options(shiny.testmode = TRUE)
+#'
+#' # This application shows the test endpoint URL; clicking on it will
+#' # fetch the input, output, and exported values in JSON format.
+#' shinyApp(
+#'   ui = basicPage(
+#'     h4("Snapshot URL: "),
+#'     uiOutput("url"),
+#'     h4("Current values:"),
+#'     verbatimTextOutput("values"),
+#'     actionButton("inc", "Increment x")
+#'   ),
+#'
+#'   server = function(input, output, session) {
+#'     vals <- reactiveValues(x = 1)
+#'     y <- reactive({ vals$x + 1 })
+#'
+#'     observeEvent(input$inc, {
+#'       vals$x <<- vals$x + 1
+#'     })
+#'
+#'     exportTestValues(
+#'       x = vals$x,
+#'       y = y()
+#'     )
+#'
+#'     output$url <- renderUI({
+#'       url <- session$getTestEndpointUrl(format="json")
+#'       a(href = url, url)
+#'     })
+#'
+#'     output$values <- renderText({
+#'       paste0("vals$x: ", vals$x, "\ny: ", y())
+#'     })
+#'   }
+#' )
+#' }
+#' @export
+exportTestValues <- function(..., quoted_ = FALSE, env_ = parent.frame(),
+  session_ = getDefaultReactiveDomain())
+{
+  session_$exportTestValues(..., quoted_ = quoted_, env_ = env_)
+}
diff --git a/R/timer.R b/R/timer.R
index b501464..51f237b 100644
--- a/R/timer.R
+++ b/R/timer.R
@@ -22,6 +22,11 @@ TimerCallbacks <- R6Class(
       .times <<- data.frame()
     },
     schedule = function(millis, func) {
+      # If args could fail to evaluate, let's make them do that before
+      # we change any state
+      force(millis)
+      force(func)
+
       id <- .nextId
       .nextId <<- .nextId + 1L
 
@@ -56,7 +61,7 @@ TimerCallbacks <- R6Class(
     },
     executeElapsed = function() {
       elapsed <- takeElapsed()
-      if (length(elapsed) == 0)
+      if (nrow(elapsed) == 0)
         return(FALSE)
 
       for (id in elapsed$id) {
@@ -71,3 +76,16 @@ TimerCallbacks <- R6Class(
 )
 
 timerCallbacks <- TimerCallbacks$new()
+
+scheduleTask <- function(millis, callback) {
+  cancelled <- FALSE
+  timerCallbacks$schedule(millis, function() {
+    if (!cancelled)
+      callback()
+  })
+
+  function() {
+    cancelled <<- TRUE
+    callback <<- NULL # to allow for callback to be gc'ed
+  }
+}
diff --git a/R/update-input.R b/R/update-input.R
index 17aba81..fa58f68 100644
--- a/R/update-input.R
+++ b/R/update-input.R
@@ -6,9 +6,16 @@
 #' @seealso \code{\link{textInput}}
 #'
 #' @examples
-#' \dontrun{
-#' shinyServer(function(input, output, session) {
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   sliderInput("controller", "Controller", 0, 20, 10),
+#'   textInput("inText", "Input text"),
+#'   textInput("inText2", "Input text 2")
+#' )
 #'
+#' server <- function(input, output, session) {
 #'   observe({
 #'     # We'll use the input$controller variable multiple times, so save it as x
 #'     # for convenience.
@@ -22,7 +29,9 @@
 #'       label = paste("New label", x),
 #'       value = paste("New text", x))
 #'   })
-#' })
+#' }
+#'
+#' shinyApp(ui, server)
 #' }
 #' @export
 updateTextInput <- function(session, inputId, label = NULL, value = NULL) {
@@ -30,6 +39,44 @@ updateTextInput <- function(session, inputId, label = NULL, value = NULL) {
   session$sendInputMessage(inputId, message)
 }
 
+#' Change the value of a textarea input on the client
+#'
+#' @template update-input
+#' @param value The value to set for the input object.
+#'
+#' @seealso \code{\link{textAreaInput}}
+#'
+#' @examples
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   sliderInput("controller", "Controller", 0, 20, 10),
+#'   textAreaInput("inText", "Input textarea"),
+#'   textAreaInput("inText2", "Input textarea 2")
+#' )
+#'
+#' server <- function(input, output, session) {
+#'   observe({
+#'     # We'll use the input$controller variable multiple times, so save it as x
+#'     # for convenience.
+#'     x <- input$controller
+#'
+#'     # This will change the value of input$inText, based on x
+#'     updateTextAreaInput(session, "inText", value = paste("New text", x))
+#'
+#'     # Can also set the label, this time for input$inText2
+#'     updateTextAreaInput(session, "inText2",
+#'       label = paste("New label", x),
+#'       value = paste("New text", x))
+#'   })
+#' }
+#'
+#' shinyApp(ui, server)
+#' }
+#' @export
+updateTextAreaInput <- updateTextInput
+
 
 #' Change the value of a checkbox input on the client
 #'
@@ -39,26 +86,87 @@ updateTextInput <- function(session, inputId, label = NULL, value = NULL) {
 #' @seealso \code{\link{checkboxInput}}
 #'
 #' @examples
-#' \dontrun{
-#' shinyServer(function(input, output, session) {
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   sliderInput("controller", "Controller", 0, 1, 0, step = 1),
+#'   checkboxInput("inCheckbox", "Input checkbox")
+#' )
 #'
+#' server <- function(input, output, session) {
 #'   observe({
-#'     # TRUE if input$controller is even, FALSE otherwise.
-#'     x_even <- input$controller %% 2 == 0
+#'     # TRUE if input$controller is odd, FALSE if even.
+#'     x_even <- input$controller %% 2 == 1
 #'
 #'     updateCheckboxInput(session, "inCheckbox", value = x_even)
 #'   })
-#' })
+#' }
+#'
+#' shinyApp(ui, server)
 #' }
 #' @export
 updateCheckboxInput <- updateTextInput
 
 
+#' Change the label or icon of an action button on the client
+#'
+#' @template update-input
+#' @param icon The icon to set for the input object. To remove the
+#' current icon, use \code{icon=character(0)}.
+#'
+#' @seealso \code{\link{actionButton}}
+#'
+#' @examples
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   actionButton("update", "Update other buttons"),
+#'   br(),
+#'   actionButton("goButton", "Go"),
+#'   br(),
+#'   actionButton("goButton2", "Go 2", icon = icon("area-chart")),
+#'   br(),
+#'   actionButton("goButton3", "Go 3")
+#' )
+#'
+#' server <- function(input, output, session) {
+#'   observe({
+#'     req(input$update)
+#'
+#'     # Updates goButton's label and icon
+#'     updateActionButton(session, "goButton",
+#'       label = "New label",
+#'       icon = icon("calendar"))
+#'
+#'     # Leaves goButton2's label unchaged and
+#'     # removes its icon
+#'     updateActionButton(session, "goButton2",
+#'       icon = character(0))
+#'
+#'     # Leaves goButton3's icon, if it exists,
+#'     # unchaged and changes its label
+#'     updateActionButton(session, "goButton3",
+#'       label = "New label 3")
+#'   })
+#' }
+#'
+#' shinyApp(ui, server)
+#' }
+#' @export
+updateActionButton <- function(session, inputId, label = NULL, icon = NULL) {
+  if (!is.null(icon)) icon <- as.character(validateIcon(icon))
+  message <- dropNulls(list(label=label, icon=icon))
+  session$sendInputMessage(inputId, message)
+}
+
+
 #' Change the value of a date input on the client
 #'
 #' @template update-input
 #' @param value The desired date value. Either a Date object, or a string in
-#'   \code{yyyy-mm-dd} format.
+#'   \code{yyyy-mm-dd} format. Supply \code{NA} to clear the date.
 #' @param min The minimum allowed date. Either a Date object, or a string in
 #'   \code{yyyy-mm-dd} format.
 #' @param max The maximum allowed date. Either a Date object, or a string in
@@ -67,32 +175,44 @@ updateCheckboxInput <- updateTextInput
 #' @seealso \code{\link{dateInput}}
 #'
 #' @examples
-#' \dontrun{
-#' shinyServer(function(input, output, session) {
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
 #'
-#'   observe({
-#'     # We'll use the input$controller variable multiple times, so save it as x
-#'     # for convenience.
-#'     x <- input$controller
+#' ui <- fluidPage(
+#'   sliderInput("n", "Day of month", 1, 30, 10),
+#'   dateInput("inDate", "Input date")
+#' )
 #'
+#' server <- function(input, output, session) {
+#'   observe({
+#'     date <- as.Date(paste0("2013-04-", input$n))
 #'     updateDateInput(session, "inDate",
-#'       label = paste("Date label", x),
-#'       value = paste("2013-04-", x, sep=""),
-#'       min   = paste("2013-04-", x-1, sep=""),
-#'       max   = paste("2013-04-", x+1, sep="")
+#'       label = paste("Date label", input$n),
+#'       value = date,
+#'       min   = date - 3,
+#'       max   = date + 3
 #'     )
 #'   })
-#' })
+#' }
+#'
+#' shinyApp(ui, server)
 #' }
 #' @export
 updateDateInput <- function(session, inputId, label = NULL, value = NULL,
                             min = NULL, max = NULL) {
 
-  # If value is a date object, convert it to a string with yyyy-mm-dd format
-  # Same for min and max
-  if (inherits(value, "Date"))  value <- format(value, "%Y-%m-%d")
-  if (inherits(min, "Date"))    min   <- format(min,   "%Y-%m-%d")
-  if (inherits(max, "Date"))    max   <- format(max,   "%Y-%m-%d")
+  # Make sure values are NULL or Date objects. This is so we can ensure that
+  # they will be formatted correctly. For example, the string "2016-08-9" is not
+  # correctly formatted, but the conversion to Date and back to string will fix
+  # it.
+  formatDate <- function(x) {
+    if (is.null(x))
+      return(NULL)
+    format(as.Date(x), "%Y-%m-%d")
+  }
+  value <- formatDate(value)
+  min   <- formatDate(min)
+  max   <- formatDate(max)
 
   message <- dropNulls(list(label=label, value=value, min=min, max=max))
   session$sendInputMessage(inputId, message)
@@ -103,9 +223,9 @@ updateDateInput <- function(session, inputId, label = NULL, value = NULL,
 #'
 #' @template update-input
 #' @param start The start date. Either a Date object, or a string in
-#'   \code{yyyy-mm-dd} format.
+#'   \code{yyyy-mm-dd} format. Supplying \code{NA} clears the start date.
 #' @param end The end date. Either a Date object, or a string in
-#'   \code{yyyy-mm-dd} format.
+#'   \code{yyyy-mm-dd} format. Supplying \code{NA} clears the end date.
 #' @param min The minimum allowed date. Either a Date object, or a string in
 #'   \code{yyyy-mm-dd} format.
 #' @param max The maximum allowed date. Either a Date object, or a string in
@@ -114,20 +234,29 @@ updateDateInput <- function(session, inputId, label = NULL, value = NULL,
 #' @seealso \code{\link{dateRangeInput}}
 #'
 #' @examples
-#' \dontrun{
-#' shinyServer(function(input, output, session) {
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   sliderInput("n", "Day of month", 1, 30, 10),
+#'   dateRangeInput("inDateRange", "Input date range")
+#' )
 #'
+#' server <- function(input, output, session) {
 #'   observe({
-#'     # We'll use the input$controller variable multiple times, so save it as x
-#'     # for convenience.
-#'     x <- input$controller
+#'     date <- as.Date(paste0("2013-04-", input$n))
 #'
 #'     updateDateRangeInput(session, "inDateRange",
-#'       label = paste("Date range label", x),
-#'       start = paste("2013-01-", x, sep=""))
-#'       end = paste("2013-12-", x, sep=""))
+#'       label = paste("Date range label", input$n),
+#'       start = date - 1,
+#'       end = date + 1,
+#'       min = date - 5,
+#'       max = date + 5
+#'     )
 #'   })
-#' })
+#' }
+#'
+#' shinyApp(ui, server)
 #' }
 #' @export
 updateDateRangeInput <- function(session, inputId, label = NULL,
@@ -142,7 +271,7 @@ updateDateRangeInput <- function(session, inputId, label = NULL,
 
   message <- dropNulls(list(
     label = label,
-    value = c(start, end),
+    value = dropNulls(list(start = start, end = end)),
     min = min,
     max = max
   ))
@@ -162,22 +291,31 @@ updateDateRangeInput <- function(session, inputId, label = NULL,
 #' \code{\link{navbarPage}}
 #'
 #' @examples
-#' \dontrun{
-#' shinyServer(function(input, output, session) {
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
 #'
-#'   observe({
-#'     # TRUE if input$controller is even, FALSE otherwise.
-#'     x_even <- input$controller %% 2 == 0
-#'
-#'     # Change the selected tab.
-#'     # Note that the tabset container must have been created with an 'id' argument
-#'     if (x_even) {
-#'       updateTabsetPanel(session, "inTabset", selected = "panel2")
-#'     } else {
-#'       updateTabsetPanel(session, "inTabset", selected = "panel1")
-#'     }
+#' ui <- fluidPage(sidebarLayout(
+#'   sidebarPanel(
+#'     sliderInput("controller", "Controller", 1, 3, 1)
+#'   ),
+#'   mainPanel(
+#'     tabsetPanel(id = "inTabset",
+#'       tabPanel(title = "Panel 1", value = "panel1", "Panel 1 content"),
+#'       tabPanel(title = "Panel 2", value = "panel2", "Panel 2 content"),
+#'       tabPanel(title = "Panel 3", value = "panel3", "Panel 3 content")
+#'     )
+#'   )
+#' ))
+#'
+#' server <- function(input, output, session) {
+#'   observeEvent(input$controller, {
+#'     updateTabsetPanel(session, "inTabset",
+#'       selected = paste0("panel", input$controller)
+#'     )
 #'   })
-#' })
+#' }
+#'
+#' shinyApp(ui, server)
 #' }
 #' @export
 updateTabsetPanel <- function(session, inputId, selected = NULL) {
@@ -204,10 +342,18 @@ updateNavlistPanel <- updateTabsetPanel
 #' @seealso \code{\link{numericInput}}
 #'
 #' @examples
-#' \dontrun{
-#' shinyServer(function(input, output, session) {
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
 #'
-#'   observe({
+#' ui <- fluidPage(
+#'   sliderInput("controller", "Controller", 0, 20, 10),
+#'   numericInput("inNumber", "Input number", 0),
+#'   numericInput("inNumber2", "Input number 2", 0)
+#' )
+#'
+#' server <- function(input, output, session) {
+#'
+#'   observeEvent(input$controller, {
 #'     # We'll use the input$controller variable multiple times, so save it as x
 #'     # for convenience.
 #'     x <- input$controller
@@ -218,7 +364,9 @@ updateNavlistPanel <- updateTabsetPanel
 #'       label = paste("Number label ", x),
 #'       value = x, min = x-10, max = x+10, step = 5)
 #'   })
-#' })
+#' }
+#'
+#' shinyApp(ui, server)
 #' }
 #' @export
 updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
@@ -309,11 +457,11 @@ updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
   if (!is.null(choices))
     choices <- choicesWithNames(choices)
   if (!is.null(selected))
-    selected <- validateSelected(selected, choices, inputId)
+    selected <- validateSelected(selected, choices, session$ns(inputId))
 
   options <- if (!is.null(choices)) {
     format(tagList(
-      generateOptions(inputId, choices, selected, inline, type = type)
+      generateOptions(session$ns(inputId), choices, selected, inline, type = type)
     ))
   }
 
@@ -330,31 +478,35 @@ updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
 #' @seealso \code{\link{checkboxGroupInput}}
 #'
 #' @examples
-#' \dontrun{
-#' shinyServer(function(input, output, session) {
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
 #'
-#'   observe({
-#'     # We'll use the input$controller variable multiple times, so save it as x
-#'     # for convenience.
-#'     x <- input$controller
+#' ui <- fluidPage(
+#'   p("The first checkbox group controls the second"),
+#'   checkboxGroupInput("inCheckboxGroup", "Input checkbox",
+#'     c("Item A", "Item B", "Item C")),
+#'   checkboxGroupInput("inCheckboxGroup2", "Input checkbox 2",
+#'     c("Item A", "Item B", "Item C"))
+#' )
 #'
-#'     # Create a list of new options, where the name of the items is something
-#'     # like 'option label x 1', and the values are 'option-x-1'.
-#'     cb_options <- list()
-#'     cb_options[[sprintf("option label %d 1", x)]] <- sprintf("option-%d-1", x)
-#'     cb_options[[sprintf("option label %d 2", x)]] <- sprintf("option-%d-2", x)
+#' server <- function(input, output, session) {
+#'   observe({
+#'     x <- input$inCheckboxGroup
 #'
-#'     # Change values for input$inCheckboxGroup
-#'     updateCheckboxGroupInput(session, "inCheckboxGroup", choices = cb_options)
+#'     # Can use character(0) to remove all choices
+#'     if (is.null(x))
+#'       x <- character(0)
 #'
 #'     # Can also set the label and select items
 #'     updateCheckboxGroupInput(session, "inCheckboxGroup2",
-#'       label = paste("checkboxgroup label", x),
-#'       choices = cb_options,
-#'       selected = sprintf("option-%d-2", x)
+#'       label = paste("Checkboxgroup label", length(x)),
+#'       choices = x,
+#'       selected = x
 #'     )
 #'   })
-#' })
+#' }
+#'
+#' shinyApp(ui, server)
 #' }
 #' @export
 updateCheckboxGroupInput <- function(session, inputId, label = NULL,
@@ -372,29 +524,31 @@ updateCheckboxGroupInput <- function(session, inputId, label = NULL,
 #' @seealso \code{\link{radioButtons}}
 #'
 #' @examples
-#' \dontrun{
-#' shinyServer(function(input, output, session) {
-#'
-#'   observe({
-#'     # We'll use the input$controller variable multiple times, so save it as x
-#'     # for convenience.
-#'     x <- input$controller
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
 #'
-#'     r_options <- list()
-#'     r_options[[sprintf("option label %d 1", x)]] <- sprintf("option-%d-1", x)
-#'     r_options[[sprintf("option label %d 2", x)]] <- sprintf("option-%d-2", x)
+#' ui <- fluidPage(
+#'   p("The first radio button group controls the second"),
+#'   radioButtons("inRadioButtons", "Input radio buttons",
+#'     c("Item A", "Item B", "Item C")),
+#'   radioButtons("inRadioButtons2", "Input radio buttons 2",
+#'     c("Item A", "Item B", "Item C"))
+#' )
 #'
-#'     # Change values for input$inRadio
-#'     updateRadioButtons(session, "inRadio", choices = r_options)
+#' server <- function(input, output, session) {
+#'   observe({
+#'     x <- input$inRadioButtons
 #'
-#'     # Can also set the label and select an item
-#'     updateRadioButtons(session, "inRadio2",
-#'       label = paste("Radio label", x),
-#'       choices = r_options,
-#'       selected = sprintf("option-%d-2", x)
+#'     # Can also set the label and select items
+#'     updateRadioButtons(session, "inRadioButtons2",
+#'       label = paste("radioButtons label", x),
+#'       choices = x,
+#'       selected = x
 #'     )
 #'   })
-#' })
+#' }
+#'
+#' shinyApp(ui, server)
 #' }
 #' @export
 updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
@@ -413,32 +567,35 @@ updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
 #' @seealso \code{\link{selectInput}}
 #'
 #' @examples
-#' \dontrun{
-#' shinyServer(function(input, output, session) {
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
+#'   p("The checkbox group controls the select input"),
+#'   checkboxGroupInput("inCheckboxGroup", "Input checkbox",
+#'     c("Item A", "Item B", "Item C")),
+#'   selectInput("inSelect", "Select input",
+#'     c("Item A", "Item B", "Item C"))
+#' )
 #'
+#' server <- function(input, output, session) {
 #'   observe({
-#'     # We'll use the input$controller variable multiple times, so save it as x
-#'     # for convenience.
-#'     x <- input$controller
+#'     x <- input$inCheckboxGroup
+#'
+#'     # Can use character(0) to remove all choices
+#'     if (is.null(x))
+#'       x <- character(0)
 #'
-#'     # Create a list of new options, where the name of the items is something
-#'     # like 'option label x 1', and the values are 'option-x-1'.
-#'     s_options <- list()
-#'     s_options[[sprintf("option label %d 1", x)]] <- sprintf("option-%d-1", x)
-#'     s_options[[sprintf("option label %d 2", x)]] <- sprintf("option-%d-2", x)
-#'
-#'     # Change values for input$inSelect
-#'     updateSelectInput(session, "inSelect", choices = s_options)
-#'
-#'     # Can also set the label and select an item (or more than one if it's a
-#'     # multi-select)
-#'     updateSelectInput(session, "inSelect2",
-#'       label = paste("Select label", x),
-#'       choices = s_options,
-#'       selected = sprintf("option-%d-2", x)
+#'     # Can also set the label and select items
+#'     updateSelectInput(session, "inSelect",
+#'       label = paste("Select input label", length(x)),
+#'       choices = x,
+#'       selected = tail(x, 1)
 #'     )
 #'   })
-#' })
+#' }
+#'
+#' shinyApp(ui, server)
 #' }
 #' @export
 updateSelectInput <- function(session, inputId, label = NULL, choices = NULL,
@@ -465,7 +622,7 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
     res <- checkAsIs(options)
     cfg <- tags$script(
       type = 'application/json',
-      `data-for` = inputId,
+      `data-for` = session$ns(inputId),
       `data-eval` = if (length(res$eval)) HTML(toJSON(res$eval)),
       HTML(toJSON(res$options))
     )
diff --git a/R/utils.R b/R/utils.R
index 135adf1..1d8961b 100644
--- a/R/utils.R
+++ b/R/utils.R
@@ -23,7 +23,6 @@ NULL
 #' 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 = stats::runif(1, 0, .Machine$integer.max)) {
   force(seed)
@@ -155,6 +154,20 @@ dropNullsOrEmpty <- function(x) {
   x[!vapply(x, nullOrEmpty, FUN.VALUE=logical(1))]
 }
 
+# Given a vector/list, return TRUE if any elements are named, FALSE otherwise.
+anyNamed <- function(x) {
+  # Zero-length vector
+  if (length(x) == 0) return(FALSE)
+
+  nms <- names(x)
+
+  # List with no name attribute
+  if (is.null(nms)) return(FALSE)
+
+  # List with name attribute; check for any ""
+  any(nzchar(nms))
+}
+
 # Given a vector/list, return TRUE if any elements are unnamed, FALSE otherwise.
 anyUnnamed <- function(x) {
   # Zero-length vector
@@ -169,6 +182,31 @@ anyUnnamed <- function(x) {
   any(!nzchar(nms))
 }
 
+# Given two named vectors, join them together, and keep only the last element
+# with a given name in the resulting vector. If b has any elements with the same
+# name as elements in a, the element in a is dropped. Also, if there are any
+# duplicated names in a or b, only the last one with that name is kept.
+mergeVectors <- function(a, b) {
+  if (anyUnnamed(a) || anyUnnamed(b)) {
+    stop("Vectors must be either NULL or have names for all elements")
+  }
+
+  x <- c(a, b)
+  drop_idx <- duplicated(names(x), fromLast = TRUE)
+  x[!drop_idx]
+}
+
+# Wrapper around list2env with a NULL check. In R <3.2.0, if an empty unnamed
+# list is passed to list2env(), it errors. But an empty named list is OK. For
+# R >=3.2.0, this wrapper is not necessary.
+list2env2 <- function(x, ...) {
+  # Ensure that zero-length lists have a name attribute
+   if (length(x) == 0)
+    attr(x, "names") <- character(0)
+
+  list2env(x, ...)
+}
+
 # Combine dir and (file)name into a file path. If a file already exists with a
 # name differing only by case, then use it instead.
 file.path.ci <- function(...) {
@@ -211,6 +249,12 @@ find.file.ci <- function(...) {
   return(matches[1])
 }
 
+# The function base::dir.exists was added in R 3.2.0, but for backward
+# compatibility we need to add this function
+dirExists <- function(paths) {
+  file.exists(paths) & file.info(paths)$isdir
+}
+
 # Attempt to join a path and relative path, and turn the result into a
 # (normalized) absolute path. The result will only be returned if it is an
 # existing file/directory and is a descendant of dir.
@@ -376,7 +420,6 @@ makeFunction <- function(args = pairlist(), body, env = parent.frame()) {
 #'
 #' isolate(tripleA())
 #' # "text, text, text"
-#'
 #' @export
 exprToFunction <- function(expr, env=parent.frame(), quoted=FALSE) {
   if (!quoted) {
@@ -410,7 +453,6 @@ exprToFunction <- function(expr, env=parent.frame(), quoted=FALSE) {
 #'   the name of the calling function.
 #' @param wrappedWithLabel,..stacktraceon Advanced use only. For stack manipulation purposes; see
 #'   \code{\link{stacktrace}}.
-#'
 #' @export
 installExprFunction <- function(expr, name, eval.env = parent.frame(2),
                                 quoted = FALSE,
@@ -454,7 +496,7 @@ installExprFunction <- function(expr, name, eval.env = parent.frame(2),
 #'
 #' \dontrun{
 #' # Example of usage within a Shiny app
-#' shinyServer(function(input, output, session) {
+#' function(input, output, session) {
 #'
 #'   output$queryText <- renderText({
 #'     query <- parseQueryString(session$clientData$url_search)
@@ -470,7 +512,7 @@ installExprFunction <- function(expr, name, eval.env = parent.frame(2),
 #'     # Return a string with key-value pairs
 #'     paste(names(query), query, sep = "=", collapse=", ")
 #'   })
-#' })
+#' }
 #' }
 #'
 parseQueryString <- function(str, nested = FALSE) {
@@ -482,6 +524,8 @@ parseQueryString <- function(str, nested = FALSE) {
     str <- substr(str, 2, nchar(str))
 
   pairs <- strsplit(str, '&', fixed = TRUE)[[1]]
+  # Drop any empty items (if there's leading/trailing/consecutive '&' chars)
+  pairs <- pairs[pairs != ""]
   pairs <- strsplit(pairs, '=', fixed = TRUE)
 
   keys   <- vapply(pairs, function(x) x[1], FUN.VALUE = character(1))
@@ -597,7 +641,10 @@ Callbacks <- R6Class(
     .callbacks = 'Map',
 
     initialize = function() {
-      .nextId <<- as.integer(.Machine$integer.max)
+      # NOTE: we avoid using '.Machine$integer.max' directly
+      # as R 3.3.0's 'radixsort' could segfault when sorting
+      # an integer vector containing this value
+      .nextId <<- as.integer(.Machine$integer.max - 1L)
       .callbacks <<- Map$new()
     },
     register = function(callback) {
@@ -647,6 +694,21 @@ dataTablesJSON <- function(data, req) {
   q <- parseQueryString(params, nested = TRUE)
   ci <- q$search[['caseInsensitive']] == 'true'
 
+  # data may have been replaced/updated in the new table while the Ajax request
+  # from the previous table is still on its way, so it is possible that the old
+  # request asks for more columns than the current data, in which case we should
+  # discard this request and return empty data; the next Ajax request from the
+  # new table will retrieve the correct number of columns of data
+  if (length(q$columns) != ncol(data)) {
+    res <- toJSON(list(
+      draw = as.integer(q$draw),
+      recordsTotal = n,
+      recordsFiltered = 0,
+      data = NULL
+    ))
+    return(httpResponse(200, 'application/json', enc2utf8(res)))
+  }
+
   # global searching
   i <- seq_len(n)
   if (length(q$search[['value']]) && q$search[['value']] != '') {
@@ -847,6 +909,143 @@ columnToRowData <- function(data) {
   )
 }
 
+#' Declare an error safe for the user to see
+#'
+#' This should be used when you want to let the user see an error
+#' message even if the default is to sanitize all errors. If you have an
+#' error \code{e} and call \code{stop(safeError(e))}, then Shiny will
+#' ignore the value of \code{getOption("shiny.sanitize.errors")} and always
+#' display the error in the app itself.
+#'
+#' @param error Either an "error" object or a "character" object (string).
+#' In the latter case, the string will become the message of the error
+#' returned by \code{safeError}.
+#'
+#' @return An "error" object
+#'
+#' @details An error generated by \code{safeError} has priority over all
+#' other Shiny errors. This can be dangerous. For example, if you have set
+#' \code{options(shiny.sanitize.errors = TRUE)}, then by default all error
+#' messages are omitted in the app, and replaced by a generic error message.
+#' However, this does not apply to \code{safeError}: whatever you pass
+#' through \code{error} will be displayed to the user. So, this should only
+#' be used when you are sure that your error message does not contain any
+#' sensitive information. In those situations, \code{safeError} can make
+#' your users' lives much easier by giving them a hint as to where the
+#' error occurred.
+#'
+#' @seealso \code{\link{shiny-options}}
+#'
+#' @examples
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' # uncomment the desired line to experiment with shiny.sanitize.errors
+#' # options(shiny.sanitize.errors = TRUE)
+#' # options(shiny.sanitize.errors = FALSE)
+#'
+#' # Define UI
+#' ui <- fluidPage(
+#'   textInput('number', 'Enter your favorite number from 1 to 10', '5'),
+#'   textOutput('normalError'),
+#'   textOutput('safeError')
+#' )
+#'
+#' # Server logic
+#' server <- function(input, output) {
+#'   output$normalError <- renderText({
+#'     number <- input$number
+#'     if (number %in% 1:10) {
+#'       return(paste('You chose', number, '!'))
+#'     } else {
+#'       stop(
+#'         paste(number, 'is not a number between 1 and 10')
+#'       )
+#'     }
+#'   })
+#'   output$safeError <- renderText({
+#'     number <- input$number
+#'     if (number %in% 1:10) {
+#'       return(paste('You chose', number, '!'))
+#'     } else {
+#'       stop(safeError(
+#'         paste(number, 'is not a number between 1 and 10')
+#'       ))
+#'     }
+#'   })
+#' }
+#'
+#' # Complete app with UI and server components
+#' shinyApp(ui, server)
+#' }
+#' @export
+safeError <- function(error) {
+  if (inherits(error, "character")) {
+    error <- simpleError(error)
+  }
+  if (!inherits(error, "error")) {
+    stop("The class of the `error` parameter must be either 'error' or 'character'")
+  }
+  class(error) <- c("shiny.custom.error", class(error))
+  error
+}
+
+#***********************************************************************#
+#**** Keep this function internal for now, may chnage in the future ****#
+#***********************************************************************#
+# #' Propagate an error through Shiny, but catch it before it throws
+# #'
+# #' Throws a type of exception that is caught by observers. When such an
+# #' exception is triggered, all reactive links are broken. So, essentially,
+# #' \code{reactiveStop()} behaves just like \code{stop()}, except that
+# #' instead of ending the session, it is silently swalowed by Shiny.
+# #'
+# #' This function should be used when you want to disrupt the reactive
+# #' links in a reactive chain, but do not want to end the session. For
+# #' example, this enables you to disallow certain inputs, but get back
+# #' to business as usual when valid inputs are re-entered.
+# #' \code{reactiveStop} is also called internally by Shiny to create
+# #' special errors, such as the ones generated by \code{\link{validate}()},
+# #' \code{\link{req}()} and \code{\link{cancelOutput}()}.
+# #'
+# #' @param message An optional error message.
+# #' @param class An optional class to add to the error.
+# #' @export
+# #' @examples
+# #' ## Note: the breaking of the reactive chain that happens in the app
+# #' ## below (when input$txt = 'bad' and input$allowBad = 'FALSE') is
+# #' ## easily visualized with `showReactLog()`
+# #'
+# #' ## Only run examples in interactive R sessions
+# #' if (interactive()) {
+# #'
+# #' ui <- fluidPage(
+# #'   textInput('txt', 'Enter some text...'),
+# #'   selectInput('allowBad', 'Allow the string \'bad\'?',
+# #'               c('TRUE', 'FALSE'), selected = 'FALSE')
+# #' )
+# #'
+# #' server <- function(input, output) {
+# #'   val <- reactive({
+# #'     if (!(as.logical(input$allowBad))) {
+# #'       if (identical(input$txt, "bad")) {
+# #'         reactiveStop()
+# #'       }
+# #'     }
+## '   })
+# #'
+# #'   observe({
+# #'     val()
+# #'   })
+# #' }
+# #'
+# #' shinyApp(ui, server)
+# #' }
+# #' @export
+reactiveStop <- function(message = "", class = NULL) {
+  stopWithCondition(c("shiny.silent.error", class), message)
+}
+
 #' Validate input values and other conditions
 #'
 #' For an output rendering function (e.g. \code{\link{renderPlot}()}), you may
@@ -903,15 +1102,16 @@ columnToRowData <- function(data) {
 #'   \code{shiny-output-error-} prepended to this value.
 #' @export
 #' @examples
-#' # in ui.R
-#' fluidPage(
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' ui <- fluidPage(
 #'   checkboxGroupInput('in1', 'Check some letters', choices = head(LETTERS)),
 #'   selectizeInput('in2', 'Select a state', choices = state.name),
 #'   plotOutput('plot')
 #' )
 #'
-#' # in server.R
-#' function(input, output) {
+#' server <- function(input, output) {
 #'   output$plot <- renderPlot({
 #'     validate(
 #'       need(input$in1, 'Check at least one letter!'),
@@ -920,6 +1120,10 @@ columnToRowData <- function(data) {
 #'     plot(1:10, main = paste(c(input$in1, input$in2), collapse = ', '))
 #'   })
 #' }
+#'
+#' shinyApp(ui, server)
+#'
+#' }
 validate <- function(..., errorClass = character(0)) {
   results <- sapply(list(...), function(x) {
     # Detect NULL or NA
@@ -940,8 +1144,7 @@ validate <- function(..., errorClass = character(0)) {
   # There may be empty strings remaining; these are message-less failures that
   # started as FALSE
   results <- results[nzchar(results)]
-
-  stopWithCondition(c("validation", errorClass), paste(results, collapse="\n"))
+  reactiveStop(paste(results, collapse="\n"), c(errorClass, "validation"))
 }
 
 #' @param expr An expression to test. The condition will pass if the expression
@@ -1035,14 +1238,69 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
 #'
 #' \code{req(input$a != 0)}
 #'
+#' \strong{Using \code{req(FALSE)}}
+#'
+#' You can use \code{req(FALSE)} (i.e. no condition) if you've already performed
+#' all the checks you needed to by that point and just want to stop the reactive
+#' chain now. There is no advantange to this, except perhaps ease of readibility
+#' if you have a complicated condition to check for (or perhaps if you'd like to
+#' divide your condition into nested \code{if} statements).
+#'
+#' \strong{Using \code{cancelOutput = TRUE}}
+#'
+#' When \code{req(..., cancelOutput = TRUE)} is used, the "silent" exception is
+#' also raised, but it is treated slightly differently if one or more outputs are
+#' currently being evaluated. In those cases, the reactive chain does not proceed
+#' or update, but the output(s) are left is whatever state they happen to be in
+#' (whatever was their last valid state).
+#'
+#' Note that this is always going to be the case if
+#' this is used inside an output context (e.g. \code{output$txt <- ...}). It may
+#' or may not be the case if it is used inside a non-output context (e.g.
+#' \code{\link{reactive}}, \code{\link{observe}} or \code{\link{observeEvent}})
+#' -- depending on whether or not there is an \code{output$...} that is triggered
+#' as a result of those calls. See the examples below for concrete scenarios.
+#'
 #' @param ... Values to check for truthiness.
+#' @param cancelOutput If \code{TRUE} and an output is being evaluated, stop
+#'   processing as usual but instead of clearing the output, leave it in
+#'   whatever state it happens to be in.
+#' @param x An expression whose truthiness value we want to determine
 #' @return The first value that was passed in.
-#'
 #' @export
-req <- function(...) {
+#' @examples
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'   ui <- fluidPage(
+#'     textInput('data', 'Enter a dataset from the "datasets" package', 'cars'),
+#'     p('(E.g. "cars", "mtcars", "pressure", "faithful")'), hr(),
+#'     tableOutput('tbl')
+#'   )
+#'
+#'   server <- function(input, output) {
+#'     output$tbl <- renderTable({
+#'
+#'       ## to require that the user types something, use: `req(input$data)`
+#'       ## but better: require that input$data is valid and leave the last
+#'       ## valid table up
+#'       req(exists(input$data, "package:datasets", inherits = FALSE),
+#'           cancelOutput = TRUE)
+#'
+#'       head(get(input$data, "package:datasets", inherits = FALSE))
+#'     })
+#'   }
+#'
+#'   shinyApp(ui, server)
+#' }
+req <- function(..., cancelOutput = FALSE) {
   dotloop(function(item) {
-    if (!isTruthy(item))
-      stopWithCondition("validation", "")
+    if (!isTruthy(item)) {
+      if (isTRUE(cancelOutput)) {
+        cancelOutput()
+      } else {
+        reactiveStop(class = "validation")
+      }
+    }
   }, ...)
 
   if (!missing(..1))
@@ -1051,6 +1309,46 @@ req <- function(...) {
     invisible()
 }
 
+#***********************************************************************#
+#**** Keep this function internal for now, may chnage in the future ****#
+#***********************************************************************#
+# #' Cancel processing of the current output
+# #'
+# #' Signals an error that Shiny treats specially if an output is currently being
+# #' evaluated. Execution will stop, but rather than clearing the output (as
+# #' \code{\link{req}} does) or showing an error message (as \code{\link{stop}}
+# #' does), the output simply remains unchanged.
+# #'
+# #' If \code{cancelOutput} is called in any non-output context (like in an
+# #' \code{\link{observe}} or \code{\link{observeEvent}}), the effect is the same
+# #' as \code{\link{req}(FALSE)}.
+# #' @export
+# #' @examples
+# #' ## Only run examples in interactive R sessions
+# #' if (interactive()) {
+# #'
+# #' # uncomment the desired line to experiment with cancelOutput() vs. req()
+# #'
+# #' ui <- fluidPage(
+# #'   textInput('txt', 'Enter text'),
+# #'   textOutput('check')
+# #' )
+# #'
+# #' server <- function(input, output) {
+# #'   output$check <- renderText({
+# #'     # req(input$txt)
+# #'     if (input$txt == 'hi') return('hi')
+# #'     else if (input$txt == 'bye') return('bye')
+# #'     # else cancelOutput()
+# #'   })
+# #' }
+# #'
+# #' shinyApp(ui, server)
+# #' }
+cancelOutput <- function() {
+  reactiveStop(class = "shiny.output.cancel")
+}
+
 # Execute a function against each element of ..., but only evaluate each element
 # after the previous element has been passed to fun_. The return value of fun_
 # is discarded, and only invisible() is returned from dotloop.
@@ -1063,6 +1361,8 @@ dotloop <- function(fun_, ...) {
   invisible()
 }
 
+#' @export
+#' @rdname req
 isTruthy <- function(x) {
   if (inherits(x, 'try-error'))
     return(FALSE)
@@ -1091,7 +1391,7 @@ isTruthy <- function(x) {
 stopWithCondition <- function(class, message) {
   cond <- structure(
     list(message = message),
-    class = c(class, 'shiny.silent.error', 'error', 'condition')
+    class = c(class, 'error', 'condition')
   )
   stop(cond)
 }
@@ -1142,6 +1442,10 @@ checkEncoding <- function(file) {
             'http://shiny.rstudio.com/articles/unicode.html for more info.')
     return('UTF-8-BOM')
   }
+  x <- readChar(file, size, useBytes = TRUE)
+  if (is.na(iconv(x, 'UTF-8', 'UTF-8'))) {
+    warning('The input file ', file, ' does not seem to be encoded in UTF8')
+  }
   'UTF-8'
 }
 
@@ -1174,7 +1478,11 @@ sourceUTF8 <- function(file, envir = globalenv()) {
     file <- tempfile(); on.exit(unlink(file), add = TRUE)
     writeLines(lines, file)
   }
-  exprs <- parse(file, keep.source = FALSE, srcfile = src, encoding = enc)
+  exprs <- try(parse(file, keep.source = FALSE, srcfile = src, encoding = enc))
+  if (inherits(exprs, "try-error")) {
+    diagnoseCode(file)
+    stop("Error sourcing ", file)
+  }
 
   # Wrap the exprs in first `{`, then ..stacktraceon..(). It's only really the
   # ..stacktraceon..() that we care about, but the `{` is needed to make that
@@ -1238,3 +1546,17 @@ wrapFunctionLabel <- function(func, name, ..stacktraceon = FALSE) {
 
   relabelWrapper
 }
+
+
+# This is a very simple mutable object which only stores one value
+# (which we can set and get). Using this class is sometimes useful
+# when communicating persistent changes across functions.
+Mutable <- R6Class("Mutable",
+  private = list(
+    value = NULL
+  ),
+  public = list(
+    set = function(value) { private$value <- value },
+    get = function() { private$value }
+  )
+)
diff --git a/README.md b/README.md
index 66f38a9..a4b3add 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
 # Shiny
 
-[![Build Status](https://travis-ci.org/rstudio/shiny.svg?branch=master)](https://travis-ci.org/rstudio/shiny)
+*Travis:* [![Travis Build Status](https://travis-ci.org/rstudio/shiny.svg?branch=master)](https://travis-ci.org/rstudio/shiny)
+
+*AppVeyor:* [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/rstudio/shiny?branch=master&svg=true)](https://ci.appveyor.com/project/rstudio/shiny)
 
 Shiny is a new package from RStudio that makes it incredibly easy to build interactive web applications with R.
 
diff --git a/build/vignette.rds b/build/vignette.rds
deleted file mode 100644
index 6a4c480..0000000
Binary files a/build/vignette.rds and /dev/null differ
diff --git a/inst/doc/events.R b/inst/doc/events.R
deleted file mode 100644
index 0c8cac1..0000000
--- a/inst/doc/events.R
+++ /dev/null
@@ -1,20 +0,0 @@
-## ----echo=FALSE----------------------------------------------------------
-knitr::kable(matrix(c(
-  'shiny:connected', 'socket', 'No', 'document',
-  'shiny:disconnected', 'socket', 'No', 'document',
-  'shiny:busy', '', 'No', 'document',
-  'shiny:idle', '', 'No', 'document',
-  'shiny:inputchanged', 'name, value, inputType', 'Yes', 'document',
-  'shiny:message', 'message', 'Yes', 'document',
-  'shiny:conditional', '', 'No', 'document',
-  'shiny:bound', 'binding, bindingType', 'No', 'input/output element',
-  'shiny:unbound', 'binding, bindingType', 'No', 'input/output element',
-  'shiny:value', 'name, value, binding', 'Yes', 'output element',
-  'shiny:error', 'name, error, binding', 'Yes', 'output element',
-  'shiny:recalculating', '', 'No', 'output element',
-  'shiny:recalculated', '', 'No', 'output element',
-  'shiny:visualchange', 'visible, binding', 'No', 'output element',
-  'shiny:updateinput', 'message, binding', 'Yes', 'input element'
-), ncol = 4, byrow = TRUE,
-dimnames = list(NULL, c('Name', 'Event Properties', 'Cancelable', 'Target'))))
-
diff --git a/inst/doc/events.Rmd b/inst/doc/events.Rmd
deleted file mode 100644
index 65c0342..0000000
--- a/inst/doc/events.Rmd
+++ /dev/null
@@ -1,121 +0,0 @@
----
-title: "JavaScript Events in Shiny"
-author: "Yihui Xie"
-date: "`r Sys.Date()`"
-vignette: >
-  %\VignetteEngine{knitr::rmarkdown}
-  %\VignetteIndexEntry{JavaScript Events in Shiny}
-output: knitr:::html_vignette
----
-
-A number of JavaScript events have been supported in Shiny after v0.12.2. These events can be used to keep track of the app progress, or even manipulate the values of inputs/outputs. All event names have the prefix `shiny:`, e.g., `shiny:connected`. We can listen to these events using `.on()` in jQuery, e.g.,
-
-```javascript
-$(document).on('shiny:connected', function(event) {
-  alert('Connected to the server');
-});
-```
-
-When an event is triggered in Shiny, the `event` object may have some additional properties that can be used to query or modify the information in Shiny, as we will see later in this document. Some events can cancel the process in Shiny, e.g., stop the propogation of an input or output change to the server. Such events include `shiny:inputchanged`, `shiny:message`, `shiny:value`, `shiny:error`, `shiny:updateinput`. To cancel the Shiny process, you can use `event.preventDefault()`, e.g.,
-
-```javascript
-// no outputs will be updated since we canceled the output changes
-$(document).on('shiny:value', function(event) {
-  event.preventDefault();
-});
-```
-
-All events currently supported in Shiny are listed below. You can find a live example at https://gallery.shinyapps.io/107-events ([source](https://github.com/rstudio/shiny-examples/tree/master/107-events)).
-
-# Initial Connection and Disconnection
-
-The events `shiny:connected` and `shiny:disconnected` are triggered when an initial connection to server is established, and when a session is ended or the connection is lost for some reason, respectively.
-
-A property `socket` in the event object is used to store the web socket that is used to communicate between R and JavaScript. For example, you may query the state of the web socket via `event.socket.readyState`.
-
-# Server Status: Busy/Idle
-
-The event `shiny:busy` is triggered when something is happening on the server (e.g. an observer is running), and the event `shiny:idle` indicates when the server is idle. The event object does not carry any special properties related to Shiny.
-
-# Messages
-
-The event `shiny:inputchanged` is triggered when an input has a new value, e.g., when you click an action button, or type in a text input. The event object has properties `name` (the id of the input), `value` (the value of the input), and `inputType` (the type of the input, e.g. `shiny.action`). 
-
-For example, suppose you have a numeric input with id `foo`, you may double its value through this event:
-
-```javascript
-$(document).on('shiny:inputchanged', function(event) {
-  if (event.name === 'foo') {
-    event.value *= 2;
-  }
-});
-```
-
-The `shiny:message` is triggered when any messages are received from the server. The event has a property `message`, which is the message object (a JavaScript object).
-
-# Conditional Panels
-
-When conditional panels (see `?shiny::conditionalPanel`) are updated, the event `shiny:conditional` is triggered on the document.
-
-# Binding/Unbinding Inputs/Outputs
-
-All the events above are triggered on the document. There are a few events triggered on specific HTML elements, including the events in the following sections on input and output elements.
-
-When an input or output is bound to Shiny, the event `shiny:bound` is triggered. Similarly, there is a `shiny:unbound` event after an input/output is unbound. In these events, the event object has properties `binding` (the input/output binding object) and `bindingType` (may be `'input'` or `'output'` depending on the binding is for an input or output).
-
-# Output Events
-
-The `shiny:value` event is triggered when an output receives a value from the server. The event object has three properties: `name` (output id), `value` (output value), and `binding` (output binding).
-
-The `shiny:error` event is triggered when an error is propogated to an output. The event also has three properties like the `shiny:value` event: `name`, `error` (the error message), and `binding`.
-
-The `shiny:recalculating` and `shiny:recalculated` events are triggered before and after an output value is recalculated, respectively. Please note `shiny:recalculated` is triggered after the output value has been recalculated in R, but that does not imply the output value has been displayed on the page. Use `shiny:value` instead if you want to do something when the output value is rendered.
-
-The `shiny:visualchange` event is triggered when an output is resized, hidden, or shown. The event object has properties `visible` (`true` or `false`) and `binding` (the output binding).
-
-Since these events are triggered specifically on an output element, you may add the listener on the output element instead of the document, although the latter also works, e.g.
-
-```javascript
-$('#foo').on('shiny:value', function(event) {
-  // append a character string to the output value
-  event.value += ' Oh that is nice!';
-});
-
-// use event.target to obtain the output element
-$(document).on('shiny:value', function(event) {
-  // cancel the output of the element with id 'foo'
-  if (event.target.id === 'foo') {
-    event.preventDefault();
-  }
-});
-```
-
-# Input Events
-
-The `shiny:updateinput` event is triggered when an input is updated, e.g., when you call `updateTextInput()` in R to update the label or value of a text input. The event object has properties `message` (the update message sent from the server) and `binding` (the input binding).
-
-# Summary
-
-Here is a summary of the events. The ones that are cancelable can also be modified by users, e.g., you can change `event.value` in the `shiny:inputchanged` event, and the new `event.value` will be used as the input value (to be passed to R).
-
-```{r echo=FALSE}
-knitr::kable(matrix(c(
-  'shiny:connected', 'socket', 'No', 'document',
-  'shiny:disconnected', 'socket', 'No', 'document',
-  'shiny:busy', '', 'No', 'document',
-  'shiny:idle', '', 'No', 'document',
-  'shiny:inputchanged', 'name, value, inputType', 'Yes', 'document',
-  'shiny:message', 'message', 'Yes', 'document',
-  'shiny:conditional', '', 'No', 'document',
-  'shiny:bound', 'binding, bindingType', 'No', 'input/output element',
-  'shiny:unbound', 'binding, bindingType', 'No', 'input/output element',
-  'shiny:value', 'name, value, binding', 'Yes', 'output element',
-  'shiny:error', 'name, error, binding', 'Yes', 'output element',
-  'shiny:recalculating', '', 'No', 'output element',
-  'shiny:recalculated', '', 'No', 'output element',
-  'shiny:visualchange', 'visible, binding', 'No', 'output element',
-  'shiny:updateinput', 'message, binding', 'Yes', 'input element'
-), ncol = 4, byrow = TRUE,
-dimnames = list(NULL, c('Name', 'Event Properties', 'Cancelable', 'Target'))))
-```
-
diff --git a/inst/doc/events.html b/inst/doc/events.html
deleted file mode 100644
index c0908f4..0000000
--- a/inst/doc/events.html
+++ /dev/null
@@ -1,281 +0,0 @@
-<!DOCTYPE html>
-
-<html xmlns="http://www.w3.org/1999/xhtml">
-
-<head>
-
-<meta charset="utf-8">
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-<meta name="generator" content="pandoc" />
-<meta name="viewport" content="width=device-width, initial-scale=1">
-
-<meta name="author" content="Yihui Xie" />
-
-<meta name="date" content="2016-03-28" />
-
-<title>JavaScript Events in Shiny</title>
-
-
-
-<style type="text/css">code{white-space: pre;}</style>
-<style type="text/css">
-div.sourceCode { overflow-x: auto; }
-table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode {
-  margin: 0; padding: 0; vertical-align: baseline; border: none; }
-table.sourceCode { width: 100%; line-height: 100%; }
-td.lineNumbers { text-align: right; padding-right: 4px; padding-left: 4px; color: #aaaaaa; border-right: 1px solid #aaaaaa; }
-td.sourceCode { padding-left: 5px; }
-code > span.kw { color: #007020; font-weight: bold; } /* Keyword */
-code > span.dt { color: #902000; } /* DataType */
-code > span.dv { color: #40a070; } /* DecVal */
-code > span.bn { color: #40a070; } /* BaseN */
-code > span.fl { color: #40a070; } /* Float */
-code > span.ch { color: #4070a0; } /* Char */
-code > span.st { color: #4070a0; } /* String */
-code > span.co { color: #60a0b0; font-style: italic; } /* Comment */
-code > span.ot { color: #007020; } /* Other */
-code > span.al { color: #ff0000; font-weight: bold; } /* Alert */
-code > span.fu { color: #06287e; } /* Function */
-code > span.er { color: #ff0000; font-weight: bold; } /* Error */
-code > span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
-code > span.cn { color: #880000; } /* Constant */
-code > span.sc { color: #4070a0; } /* SpecialChar */
-code > span.vs { color: #4070a0; } /* VerbatimString */
-code > span.ss { color: #bb6688; } /* SpecialString */
-code > span.im { } /* Import */
-code > span.va { color: #19177c; } /* Variable */
-code > span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
-code > span.op { color: #666666; } /* Operator */
-code > span.bu { } /* BuiltIn */
-code > span.ex { } /* Extension */
-code > span.pp { color: #bc7a00; } /* Preprocessor */
-code > span.at { color: #7d9029; } /* Attribute */
-code > span.do { color: #ba2121; font-style: italic; } /* Documentation */
-code > span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
-code > span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
-code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
-</style>
-
-
-<link href="data:text/css;charset=utf-8,body%20%7B%0Abackground%2Dcolor%3A%20%23fff%3B%0Amargin%3A%201em%20auto%3B%0Amax%2Dwidth%3A%20800px%3B%0Aoverflow%3A%20visible%3B%0Apadding%2Dleft%3A%202em%3B%0Apadding%2Dright%3A%202em%3B%0Afont%2Dfamily%3A%20%22Helvetica%20Neue%22%2C%20Helvetica%2C%20Arial%2C%20sans%2Dserif%3B%0Afont%2Dsize%3A%2014px%3B%0Aline%2Dheight%3A%2020px%3B%0A%7D%0A%23header%20%7B%0Atext%2Dalign%3A%20center%3B%0A%7D%0A%23TOC%20%7B%0Aclear%3A%20both%3B%0Amargin%3A%200%200% [...]
-
-</head>
-
-<body>
-
-
-
-<div class="fluid-row" id="header">
-
-
-<h1 class="title">JavaScript Events in Shiny</h1>
-<h4 class="author"><em>Yihui Xie</em></h4>
-<h4 class="date"><em>2016-03-28</em></h4>
-
-</div>
-
-
-<p>A number of JavaScript events have been supported in Shiny after v0.12.2. These events can be used to keep track of the app progress, or even manipulate the values of inputs/outputs. All event names have the prefix <code>shiny:</code>, e.g., <code>shiny:connected</code>. We can listen to these events using <code>.on()</code> in jQuery, e.g.,</p>
-<div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="at">$</span>(document).<span class="at">on</span>(<span class="st">'shiny:connected'</span><span class="op">,</span> <span class="kw">function</span>(event) <span class="op">{</span>
-  <span class="at">alert</span>(<span class="st">'Connected to the server'</span>)<span class="op">;</span>
-<span class="op">}</span>)<span class="op">;</span></code></pre></div>
-<p>When an event is triggered in Shiny, the <code>event</code> object may have some additional properties that can be used to query or modify the information in Shiny, as we will see later in this document. Some events can cancel the process in Shiny, e.g., stop the propogation of an input or output change to the server. Such events include <code>shiny:inputchanged</code>, <code>shiny:message</code>, <code>shiny:value</code>, <code>shiny:error</code>, <code>shiny:updateinput</code>. To c [...]
-<div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="co">// no outputs will be updated since we canceled the output changes</span>
-<span class="at">$</span>(document).<span class="at">on</span>(<span class="st">'shiny:value'</span><span class="op">,</span> <span class="kw">function</span>(event) <span class="op">{</span>
-  <span class="va">event</span>.<span class="at">preventDefault</span>()<span class="op">;</span>
-<span class="op">}</span>)<span class="op">;</span></code></pre></div>
-<p>All events currently supported in Shiny are listed below. You can find a live example at <a href="https://gallery.shinyapps.io/107-events" class="uri">https://gallery.shinyapps.io/107-events</a> (<a href="https://github.com/rstudio/shiny-examples/tree/master/107-events">source</a>).</p>
-<div id="initial-connection-and-disconnection" class="section level1">
-<h1>Initial Connection and Disconnection</h1>
-<p>The events <code>shiny:connected</code> and <code>shiny:disconnected</code> are triggered when an initial connection to server is established, and when a session is ended or the connection is lost for some reason, respectively.</p>
-<p>A property <code>socket</code> in the event object is used to store the web socket that is used to communicate between R and JavaScript. For example, you may query the state of the web socket via <code>event.socket.readyState</code>.</p>
-</div>
-<div id="server-status-busyidle" class="section level1">
-<h1>Server Status: Busy/Idle</h1>
-<p>The event <code>shiny:busy</code> is triggered when something is happening on the server (e.g. an observer is running), and the event <code>shiny:idle</code> indicates when the server is idle. The event object does not carry any special properties related to Shiny.</p>
-</div>
-<div id="messages" class="section level1">
-<h1>Messages</h1>
-<p>The event <code>shiny:inputchanged</code> is triggered when an input has a new value, e.g., when you click an action button, or type in a text input. The event object has properties <code>name</code> (the id of the input), <code>value</code> (the value of the input), and <code>inputType</code> (the type of the input, e.g. <code>shiny.action</code>).</p>
-<p>For example, suppose you have a numeric input with id <code>foo</code>, you may double its value through this event:</p>
-<div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="at">$</span>(document).<span class="at">on</span>(<span class="st">'shiny:inputchanged'</span><span class="op">,</span> <span class="kw">function</span>(event) <span class="op">{</span>
-  <span class="cf">if</span> (<span class="va">event</span>.<span class="at">name</span> <span class="op">===</span> <span class="st">'foo'</span>) <span class="op">{</span>
-    <span class="va">event</span>.<span class="at">value</span> <span class="op">*=</span> <span class="dv">2</span><span class="op">;</span>
-  <span class="op">}</span>
-<span class="op">}</span>)<span class="op">;</span></code></pre></div>
-<p>The <code>shiny:message</code> is triggered when any messages are received from the server. The event has a property <code>message</code>, which is the message object (a JavaScript object).</p>
-</div>
-<div id="conditional-panels" class="section level1">
-<h1>Conditional Panels</h1>
-<p>When conditional panels (see <code>?shiny::conditionalPanel</code>) are updated, the event <code>shiny:conditional</code> is triggered on the document.</p>
-</div>
-<div id="bindingunbinding-inputsoutputs" class="section level1">
-<h1>Binding/Unbinding Inputs/Outputs</h1>
-<p>All the events above are triggered on the document. There are a few events triggered on specific HTML elements, including the events in the following sections on input and output elements.</p>
-<p>When an input or output is bound to Shiny, the event <code>shiny:bound</code> is triggered. Similarly, there is a <code>shiny:unbound</code> event after an input/output is unbound. In these events, the event object has properties <code>binding</code> (the input/output binding object) and <code>bindingType</code> (may be <code>'input'</code> or <code>'output'</code> depending on the binding is for an input or output).</p>
-</div>
-<div id="output-events" class="section level1">
-<h1>Output Events</h1>
-<p>The <code>shiny:value</code> event is triggered when an output receives a value from the server. The event object has three properties: <code>name</code> (output id), <code>value</code> (output value), and <code>binding</code> (output binding).</p>
-<p>The <code>shiny:error</code> event is triggered when an error is propogated to an output. The event also has three properties like the <code>shiny:value</code> event: <code>name</code>, <code>error</code> (the error message), and <code>binding</code>.</p>
-<p>The <code>shiny:recalculating</code> and <code>shiny:recalculated</code> events are triggered before and after an output value is recalculated, respectively. Please note <code>shiny:recalculated</code> is triggered after the output value has been recalculated in R, but that does not imply the output value has been displayed on the page. Use <code>shiny:value</code> instead if you want to do something when the output value is rendered.</p>
-<p>The <code>shiny:visualchange</code> event is triggered when an output is resized, hidden, or shown. The event object has properties <code>visible</code> (<code>true</code> or <code>false</code>) and <code>binding</code> (the output binding).</p>
-<p>Since these events are triggered specifically on an output element, you may add the listener on the output element instead of the document, although the latter also works, e.g.</p>
-<div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="at">$</span>(<span class="st">'#foo'</span>).<span class="at">on</span>(<span class="st">'shiny:value'</span><span class="op">,</span> <span class="kw">function</span>(event) <span class="op">{</span>
-  <span class="co">// append a character string to the output value</span>
-  <span class="va">event</span>.<span class="at">value</span> <span class="op">+=</span> <span class="st">' Oh that is nice!'</span><span class="op">;</span>
-<span class="op">}</span>)<span class="op">;</span>
-
-<span class="co">// use event.target to obtain the output element</span>
-<span class="at">$</span>(document).<span class="at">on</span>(<span class="st">'shiny:value'</span><span class="op">,</span> <span class="kw">function</span>(event) <span class="op">{</span>
-  <span class="co">// cancel the output of the element with id 'foo'</span>
-  <span class="cf">if</span> (<span class="va">event</span>.<span class="va">target</span>.<span class="at">id</span> <span class="op">===</span> <span class="st">'foo'</span>) <span class="op">{</span>
-    <span class="va">event</span>.<span class="at">preventDefault</span>()<span class="op">;</span>
-  <span class="op">}</span>
-<span class="op">}</span>)<span class="op">;</span></code></pre></div>
-</div>
-<div id="input-events" class="section level1">
-<h1>Input Events</h1>
-<p>The <code>shiny:updateinput</code> event is triggered when an input is updated, e.g., when you call <code>updateTextInput()</code> in R to update the label or value of a text input. The event object has properties <code>message</code> (the update message sent from the server) and <code>binding</code> (the input binding).</p>
-</div>
-<div id="summary" class="section level1">
-<h1>Summary</h1>
-<p>Here is a summary of the events. The ones that are cancelable can also be modified by users, e.g., you can change <code>event.value</code> in the <code>shiny:inputchanged</code> event, and the new <code>event.value</code> will be used as the input value (to be passed to R).</p>
-<table>
-<thead>
-<tr class="header">
-<th align="left">Name</th>
-<th align="left">Event Properties</th>
-<th align="left">Cancelable</th>
-<th align="left">Target</th>
-</tr>
-</thead>
-<tbody>
-<tr class="odd">
-<td align="left">shiny:connected</td>
-<td align="left">socket</td>
-<td align="left">No</td>
-<td align="left">document</td>
-</tr>
-<tr class="even">
-<td align="left">shiny:disconnected</td>
-<td align="left">socket</td>
-<td align="left">No</td>
-<td align="left">document</td>
-</tr>
-<tr class="odd">
-<td align="left">shiny:busy</td>
-<td align="left"></td>
-<td align="left">No</td>
-<td align="left">document</td>
-</tr>
-<tr class="even">
-<td align="left">shiny:idle</td>
-<td align="left"></td>
-<td align="left">No</td>
-<td align="left">document</td>
-</tr>
-<tr class="odd">
-<td align="left">shiny:inputchanged</td>
-<td align="left">name, value, inputType</td>
-<td align="left">Yes</td>
-<td align="left">document</td>
-</tr>
-<tr class="even">
-<td align="left">shiny:message</td>
-<td align="left">message</td>
-<td align="left">Yes</td>
-<td align="left">document</td>
-</tr>
-<tr class="odd">
-<td align="left">shiny:conditional</td>
-<td align="left"></td>
-<td align="left">No</td>
-<td align="left">document</td>
-</tr>
-<tr class="even">
-<td align="left">shiny:bound</td>
-<td align="left">binding, bindingType</td>
-<td align="left">No</td>
-<td align="left">input/output element</td>
-</tr>
-<tr class="odd">
-<td align="left">shiny:unbound</td>
-<td align="left">binding, bindingType</td>
-<td align="left">No</td>
-<td align="left">input/output element</td>
-</tr>
-<tr class="even">
-<td align="left">shiny:value</td>
-<td align="left">name, value, binding</td>
-<td align="left">Yes</td>
-<td align="left">output element</td>
-</tr>
-<tr class="odd">
-<td align="left">shiny:error</td>
-<td align="left">name, error, binding</td>
-<td align="left">Yes</td>
-<td align="left">output element</td>
-</tr>
-<tr class="even">
-<td align="left">shiny:recalculating</td>
-<td align="left"></td>
-<td align="left">No</td>
-<td align="left">output element</td>
-</tr>
-<tr class="odd">
-<td align="left">shiny:recalculated</td>
-<td align="left"></td>
-<td align="left">No</td>
-<td align="left">output element</td>
-</tr>
-<tr class="even">
-<td align="left">shiny:visualchange</td>
-<td align="left">visible, binding</td>
-<td align="left">No</td>
-<td align="left">output element</td>
-</tr>
-<tr class="odd">
-<td align="left">shiny:updateinput</td>
-<td align="left">message, binding</td>
-<td align="left">Yes</td>
-<td align="left">input element</td>
-</tr>
-</tbody>
-</table>
-</div>
-
-<script type="text/javascript">
-window.onload = function() {
-  var i, fig = 1, caps = document.getElementsByClassName('caption');
-  for (i = 0; i < caps.length; i++) {
-    var cap = caps[i];
-    if (cap.parentElement.className !== 'figure' || cap.nodeName !== 'P')
-      continue;
-    cap.innerHTML = '<span>Figure ' + fig + ':</span> ' + cap.innerHTML;
-    fig++;
-  }
-  fig = 1;
-  caps = document.getElementsByTagName('caption');
-  for (i = 0; i < caps.length; i++) {
-    var cap = caps[i];
-    if (cap.parentElement.nodeName !== 'TABLE') continue;
-    cap.innerHTML = '<span>Table ' + fig + ':</span> ' + cap.innerHTML;
-    fig++;
-  }
-}
-</script>
-
-
-<!-- dynamically load mathjax for compatibility with self-contained -->
-<script>
-  (function () {
-    var script = document.createElement("script");
-    script.type = "text/javascript";
-    script.src  = "https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
-    document.getElementsByTagName("head")[0].appendChild(script);
-  })();
-</script>
-
-</body>
-</html>
diff --git a/inst/examples/01_hello/rsconnect/localhost/admin/01_hello.dcf b/inst/examples/01_hello/rsconnect/localhost/admin/01_hello.dcf
deleted file mode 100644
index 53df6e9..0000000
--- a/inst/examples/01_hello/rsconnect/localhost/admin/01_hello.dcf
+++ /dev/null
@@ -1,6 +0,0 @@
-name: 01_hello
-account: admin
-server: localhost
-bundleId: 1
-url: http://localhost:3939/admin/01_hello/
-when: 1436550957.65385
diff --git a/inst/examples/01_hello/server.R b/inst/examples/01_hello/server.R
index 9a6c2cc..8cece54 100644
--- a/inst/examples/01_hello/server.R
+++ b/inst/examples/01_hello/server.R
@@ -1,7 +1,7 @@
 library(shiny)
 
 # Define server logic required to draw a histogram
-shinyServer(function(input, output) {
+function(input, output) {
 
   # Expression that generates a histogram. The expression is
   # wrapped in a call to renderPlot to indicate that:
@@ -18,4 +18,4 @@ shinyServer(function(input, output) {
     hist(x, breaks = bins, col = 'darkgray', border = 'white')
   })
 
-})
+}
diff --git a/inst/examples/01_hello/ui.R b/inst/examples/01_hello/ui.R
index ad2dc46..a4aa6f4 100644
--- a/inst/examples/01_hello/ui.R
+++ b/inst/examples/01_hello/ui.R
@@ -1,7 +1,7 @@
 library(shiny)
 
 # Define UI for application that draws a histogram
-shinyUI(fluidPage(
+fluidPage(
 
   # Application title
   titlePanel("Hello Shiny!"),
@@ -21,4 +21,4 @@ shinyUI(fluidPage(
       plotOutput("distPlot")
     )
   )
-))
+)
diff --git a/inst/examples/02_text/server.R b/inst/examples/02_text/server.R
index b814426..cacf883 100644
--- a/inst/examples/02_text/server.R
+++ b/inst/examples/02_text/server.R
@@ -3,7 +3,7 @@ library(datasets)
 
 # Define server logic required to summarize and view the selected
 # dataset
-shinyServer(function(input, output) {
+function(input, output) {
   
   # Return the requested dataset
   datasetInput <- reactive({
@@ -23,4 +23,4 @@ shinyServer(function(input, output) {
   output$view <- renderTable({
     head(datasetInput(), n = input$obs)
   })
-})
+}
diff --git a/inst/examples/02_text/ui.R b/inst/examples/02_text/ui.R
index 97faf51..35b793e 100644
--- a/inst/examples/02_text/ui.R
+++ b/inst/examples/02_text/ui.R
@@ -1,7 +1,7 @@
 library(shiny)
 
 # Define UI for dataset viewer application
-shinyUI(fluidPage(
+fluidPage(
   
   # Application title
   titlePanel("Shiny Text"),
@@ -24,4 +24,4 @@ shinyUI(fluidPage(
       tableOutput("view")
     )
   )
-))
+)
diff --git a/inst/examples/03_reactivity/server.R b/inst/examples/03_reactivity/server.R
index 02db392..752c351 100644
--- a/inst/examples/03_reactivity/server.R
+++ b/inst/examples/03_reactivity/server.R
@@ -3,7 +3,7 @@ library(datasets)
 
 # Define server logic required to summarize and view the selected
 # dataset
-shinyServer(function(input, output) {
+function(input, output) {
 
   # By declaring datasetInput as a reactive expression we ensure 
   # that:
@@ -50,4 +50,4 @@ shinyServer(function(input, output) {
   output$view <- renderTable({
     head(datasetInput(), n = input$obs)
   })
-})
+}
diff --git a/inst/examples/03_reactivity/ui.R b/inst/examples/03_reactivity/ui.R
index c3b4672..c0824a7 100644
--- a/inst/examples/03_reactivity/ui.R
+++ b/inst/examples/03_reactivity/ui.R
@@ -1,7 +1,7 @@
 library(shiny)
 
 # Define UI for dataset viewer application
-shinyUI(fluidPage(
+fluidPage(
   
   # Application title
   titlePanel("Reactivity"),
@@ -31,4 +31,4 @@ shinyUI(fluidPage(
       tableOutput("view")
     )
   )
-))
+)
diff --git a/inst/examples/04_mpg/server.R b/inst/examples/04_mpg/server.R
index 59e8355..10b7620 100644
--- a/inst/examples/04_mpg/server.R
+++ b/inst/examples/04_mpg/server.R
@@ -11,7 +11,7 @@ mpgData$am <- factor(mpgData$am, labels = c("Automatic", "Manual"))
 
 # Define server logic required to plot various variables against
 # mpg
-shinyServer(function(input, output) {
+function(input, output) {
 
   # Compute the formula text in a reactive expression since it is
   # shared by the output$caption and output$mpgPlot functions
@@ -31,4 +31,4 @@ shinyServer(function(input, output) {
             data = mpgData,
             outline = input$outliers)
   })
-})
+}
diff --git a/inst/examples/04_mpg/ui.R b/inst/examples/04_mpg/ui.R
index ae58f62..323a169 100644
--- a/inst/examples/04_mpg/ui.R
+++ b/inst/examples/04_mpg/ui.R
@@ -1,7 +1,7 @@
 library(shiny)
 
 # Define UI for miles per gallon application
-shinyUI(fluidPage(
+fluidPage(
   
   # Application title
   titlePanel("Miles Per Gallon"),
@@ -26,4 +26,4 @@ shinyUI(fluidPage(
       plotOutput("mpgPlot")
     )
   )
-))
+)
diff --git a/inst/examples/05_sliders/server.R b/inst/examples/05_sliders/server.R
index 7670a86..a099d20 100644
--- a/inst/examples/05_sliders/server.R
+++ b/inst/examples/05_sliders/server.R
@@ -1,7 +1,7 @@
 library(shiny)
 
 # Define server logic for slider examples
-shinyServer(function(input, output) {
+function(input, output) {
   
   # Reactive expression to compose a data frame containing all of
   # the values
@@ -26,4 +26,4 @@ shinyServer(function(input, output) {
   output$values <- renderTable({
     sliderValues()
   })
-})
+}
diff --git a/inst/examples/05_sliders/ui.R b/inst/examples/05_sliders/ui.R
index 2c2f8ba..cbb7c63 100644
--- a/inst/examples/05_sliders/ui.R
+++ b/inst/examples/05_sliders/ui.R
@@ -1,7 +1,7 @@
 library(shiny)
 
 # Define UI for slider demo application
-shinyUI(fluidPage(
+fluidPage(
 
   #  Application title
   titlePanel("Sliders"),
@@ -40,4 +40,4 @@ shinyUI(fluidPage(
       tableOutput("values")
     )
   )
-))
+)
diff --git a/inst/examples/06_tabsets/server.R b/inst/examples/06_tabsets/server.R
index e3f5474..db07a2d 100644
--- a/inst/examples/06_tabsets/server.R
+++ b/inst/examples/06_tabsets/server.R
@@ -1,7 +1,7 @@
 library(shiny)
 
 # Define server logic for random distribution application
-shinyServer(function(input, output) {
+function(input, output) {
   
   # Reactive expression to generate the requested distribution.
   # This is called whenever the inputs change. The output
@@ -41,4 +41,4 @@ shinyServer(function(input, output) {
     data.frame(x=data())
   })
   
-})
+}
diff --git a/inst/examples/06_tabsets/ui.R b/inst/examples/06_tabsets/ui.R
index f739aea..732d74f 100644
--- a/inst/examples/06_tabsets/ui.R
+++ b/inst/examples/06_tabsets/ui.R
@@ -1,7 +1,7 @@
 library(shiny)
 
 # Define UI for random distribution application 
-shinyUI(fluidPage(
+fluidPage(
     
   # Application title
   titlePanel("Tabsets"),
@@ -35,4 +35,4 @@ shinyUI(fluidPage(
       )
     )
   )
-))
+)
diff --git a/inst/examples/07_widgets/server.R b/inst/examples/07_widgets/server.R
index adccd98..f3d7f7a 100644
--- a/inst/examples/07_widgets/server.R
+++ b/inst/examples/07_widgets/server.R
@@ -3,7 +3,7 @@ library(datasets)
 
 # Define server logic required to summarize and view the 
 # selected dataset
-shinyServer(function(input, output) {
+function(input, output) {
   
   # Return the requested dataset
   datasetInput <- reactive({
@@ -23,4 +23,4 @@ shinyServer(function(input, output) {
   output$view <- renderTable({
     head(datasetInput(), n = input$obs)
   })
-})
+}
diff --git a/inst/examples/07_widgets/ui.R b/inst/examples/07_widgets/ui.R
index 84888bd..5e50838 100644
--- a/inst/examples/07_widgets/ui.R
+++ b/inst/examples/07_widgets/ui.R
@@ -1,7 +1,7 @@
 library(shiny)
 
 # Define UI for dataset viewer application
-shinyUI(fluidPage(
+fluidPage(
   
   # Application title.
   titlePanel("More Widgets"),
@@ -40,4 +40,4 @@ shinyUI(fluidPage(
       tableOutput("view")
     )
   )
-))
+)
diff --git a/inst/examples/08_html/server.R b/inst/examples/08_html/server.R
index c4bf764..6abf41a 100644
--- a/inst/examples/08_html/server.R
+++ b/inst/examples/08_html/server.R
@@ -1,7 +1,7 @@
 library(shiny)
 
 # Define server logic for random distribution application
-shinyServer(function(input, output) {
+function(input, output) {
   
   # Reactive expression to generate the requested distribution. This is 
   # called whenever the inputs change. The output expressions defined 
@@ -39,4 +39,4 @@ shinyServer(function(input, output) {
     data.frame(x=data())
   })
   
-})
+}
diff --git a/inst/examples/09_upload/server.R b/inst/examples/09_upload/server.R
index 0fd7bce..869f7fe 100644
--- a/inst/examples/09_upload/server.R
+++ b/inst/examples/09_upload/server.R
@@ -1,6 +1,6 @@
 library(shiny)
 
-shinyServer(function(input, output) {
+function(input, output) {
   output$contents <- renderTable({
     
     # input$file1 will be NULL initially. After the user selects
@@ -17,4 +17,4 @@ shinyServer(function(input, output) {
     read.csv(inFile$datapath, header=input$header, sep=input$sep, 
 				 quote=input$quote)
   })
-})
+}
diff --git a/inst/examples/09_upload/ui.R b/inst/examples/09_upload/ui.R
index 947963d..a53bf1d 100644
--- a/inst/examples/09_upload/ui.R
+++ b/inst/examples/09_upload/ui.R
@@ -1,6 +1,6 @@
 library(shiny)
 
-shinyUI(fluidPage(
+fluidPage(
   titlePanel("Uploading Files"),
   sidebarLayout(
     sidebarPanel(
@@ -25,4 +25,4 @@ shinyUI(fluidPage(
       tableOutput('contents')
     )
   )
-))
+)
diff --git a/inst/examples/10_download/server.R b/inst/examples/10_download/server.R
index fa30561..9cb0f06 100644
--- a/inst/examples/10_download/server.R
+++ b/inst/examples/10_download/server.R
@@ -1,4 +1,4 @@
-shinyServer(function(input, output) {
+function(input, output) {
   datasetInput <- reactive({
     switch(input$dataset,
            "rock" = rock,
@@ -18,4 +18,4 @@ shinyServer(function(input, output) {
       write.csv(datasetInput(), file)
     }
   )
-})
+}
diff --git a/inst/examples/10_download/ui.R b/inst/examples/10_download/ui.R
index a9f1aba..0ec8f89 100644
--- a/inst/examples/10_download/ui.R
+++ b/inst/examples/10_download/ui.R
@@ -1,4 +1,4 @@
-shinyUI(fluidPage(
+fluidPage(
   titlePanel('Downloading Data'),
   sidebarLayout(
     sidebarPanel(
@@ -10,4 +10,4 @@ shinyUI(fluidPage(
       tableOutput('table')
     )
   )
-))
\ No newline at end of file
+)
diff --git a/inst/examples/11_timer/server.R b/inst/examples/11_timer/server.R
index bed7e5e..845d990 100644
--- a/inst/examples/11_timer/server.R
+++ b/inst/examples/11_timer/server.R
@@ -1,6 +1,6 @@
-shinyServer(function(input, output, session) {
+function(input, output, session) {
   output$currentTime <- renderText({
     invalidateLater(1000, session)
     paste("The current time is", Sys.time())
   })
-})
+}
diff --git a/inst/examples/11_timer/ui.R b/inst/examples/11_timer/ui.R
index 8755a32..3955c68 100644
--- a/inst/examples/11_timer/ui.R
+++ b/inst/examples/11_timer/ui.R
@@ -1,3 +1,3 @@
-shinyUI(fluidPage(
+fluidPage(
   textOutput("currentTime")
-))
\ No newline at end of file
+)
diff --git a/inst/staticdocs/index.r b/inst/staticdocs/index.r
index 3c20484..0b633dc 100644
--- a/inst/staticdocs/index.r
+++ b/inst/staticdocs/index.r
@@ -44,7 +44,10 @@ sd_section("UI Inputs",
     "sliderInput",
     "submitButton",
     "textInput",
+    "textAreaInput",
     "passwordInput",
+    "modalButton",
+    "updateActionButton",
     "updateCheckboxGroupInput",
     "updateCheckboxInput",
     "updateDateInput",
@@ -54,7 +57,9 @@ sd_section("UI Inputs",
     "updateSelectInput",
     "updateSliderInput",
     "updateTabsetPanel",
-    "updateTextInput"
+    "updateTextInput",
+    "updateTextAreaInput",
+    "updateQueryString"
   )
 )
 sd_section("UI Outputs",
@@ -68,7 +73,11 @@ sd_section("UI Outputs",
     "verbatimTextOutput",
     "downloadButton",
     "Progress",
-    "withProgress"
+    "withProgress",
+    "modalDialog",
+    "urlModal",
+    "showModal",
+    "showNotification"
   )
 )
 sd_section("Interface builder functions",
@@ -83,7 +92,9 @@ sd_section("Interface builder functions",
     "withTags",
     "htmlTemplate",
     "bootstrapLib",
-    "suppressDependencies"
+    "suppressDependencies",
+    "insertUI",
+    "removeUI"
   )
 )
 sd_section("Rendering functions",
@@ -119,6 +130,7 @@ sd_section("Reactive constructs",
     "reactiveTimer",
     "reactiveValues",
     "reactiveValuesToList",
+    "freezeReactiveValue",
     "domains",
     "showReactLog"
   )
@@ -142,6 +154,16 @@ sd_section("Running",
     "viewer"
   )
 )
+sd_section("Bookmarking state",
+  "Functions that are used for bookmarking and restoring state.",
+  c(
+    "bookmarkButton",
+    "enableBookmarking",
+    "setBookmarkExclude",
+    "showBookmarkUrlModal",
+    "onBookmark"
+  )
+)
 sd_section("Extending Shiny",
   "Functions that are intended to be called by third-party packages that extend Shiny.",
   c(
@@ -158,10 +180,16 @@ sd_section("Utility functions",
     "req",
     "validate",
     "session",
+    "shinyOptions",
+    "safeError",
+    "onFlush",
+    "restoreInput",
+    "applyInputHandlers",
     "exprToFunction",
     "installExprFunction",
     "parseQueryString",
     "plotPNG",
+    "exportTestValues",
     "repeatable",
     "shinyDeprecated",
     "serverInfo",
diff --git a/inst/www/reactive-graph.html b/inst/www/reactive-graph.html
index 38e9332..d943889 100644
--- a/inst/www/reactive-graph.html
+++ b/inst/www/reactive-graph.html
@@ -14,9 +14,9 @@ html, body {
   padding: 0;
 }
 div {
-  -moz-user-select: none; 
-  -khtml-user-select: none; 
-  -webkit-user-select: none; 
+  -moz-user-select: none;
+  -khtml-user-select: none;
+  -webkit-user-select: none;
   -o-user-select: none;
   cursor: default;
 }
@@ -41,7 +41,13 @@ svg {
 .node {
   cursor: pointer;
 }
-.node text {
+.node .tspanTime{
+  font-weight: bold;
+  font-size: 87%;
+  user-select: none;
+  transition: fill 0.75s ease;
+}
+.node .tspanLabel{
   font-family: 'Source Code Pro', monospace;
   font-weight: normal;
   text-anchor: start;
@@ -49,18 +55,18 @@ svg {
   user-select: none;
   transition: fill 0.75s ease;
 }
-.node.running text {
+.node.running .tspanLabel {
   fill: black;
 }
-.node.changed text {
+.node.changed .tspanLabel {
   fill: red;
 }
-.node text tspan {
+.node .tspanLabel {
   white-space: pre;
 }
 .node path {
   fill: white;
-  stroke: #777;
+  stroke: #999;
   stroke-width: 7.5px;
   transition: fill 0.75s ease;
 }
@@ -77,6 +83,9 @@ svg {
 .node.running path {
   fill: #61B97E;
 }
+.node.fixed path {
+  stroke: #000;
+}
 #legend {
   font-size: 22px;
   position: fixed;
@@ -148,110 +157,849 @@ svg {
 </style>
 <script>
 var log = [
-  { "action" : "valueChange", "id" : "names(input)", "value" : " chr \"dataset\"" },
-  { "action" : "valueChange", "id" : "input (all)", "value" : "List of 1\n $ dataset: chr \"rock\"" },
-  { "action" : "valueChange", "id" : "input$dataset", "value" : " chr \"rock\"" },
-  { "action" : "valueChange", "id" : "names(input)", "value" : " chr [1:2] \"caption\" \"dataset\"" },
-  { "action" : "valueChange", "id" : "input (all)", "value" : "List of 2\n $ caption: chr \"Data Summary\"\n $ dataset: chr \"rock\"" },
-  { "action" : "valueChange", "id" : "input$caption", "value" : " chr \"Data Summary\"" },
-  { "action" : "valueChange", "id" : "names(input)", "value" : " chr [1:3] \"caption\" \"dataset\" \"obs\"" },
-  { "action" : "valueChange", "id" : "input (all)", "value" : "List of 3\n $ caption: chr \"Data Summary\"\n $ obs    : num 10\n $ dataset: chr \"rock\"" },
-  { "action" : "valueChange", "id" : "input$obs", "value" : " num 10" },
-  { "action" : "valueChange", "id" : "names(clientData)", "value" : " chr \"output_caption_hidden\"" },
-  { "action" : "valueChange", "id" : "clientData (all)", "value" : "List of 1\n $ output_caption_hidden: logi FALSE" },
-  { "action" : "valueChange", "id" : "clientData$output_caption_hidden", "value" : " logi FALSE" },
-  { "action" : "valueChange", "id" : "names(clientData)", "value" : " chr [1:2] \"output_caption_hidden\" \"output_summary_hidden\"" },
-  { "action" : "valueChange", "id" : "clientData (all)", "value" : "List of 2\n $ output_caption_hidden: logi FALSE\n $ output_summary_hidden: logi FALSE" },
-  { "action" : "valueChange", "id" : "clientData$output_summary_hidden", "value" : " logi FALSE" },
-  { "action" : "valueChange", "id" : "names(clientData)", "value" : " chr [1:3] \"output_caption_hidden\" \"output_summary_hidden\" \"output_view_hidden\"" },
-  { "action" : "valueChange", "id" : "clientData (all)", "value" : "List of 3\n $ output_view_hidden   : logi FALSE\n $ output_caption_hidden: logi FALSE\n $ output_summary_hidden: logi FALSE" },
-  { "action" : "valueChange", "id" : "clientData$output_view_hidden", "value" : " logi FALSE" },
-  { "action" : "valueChange", "id" : "names(clientData)", "value" : " chr [1:4] \"output_caption_hidden\" \"output_summary_hidden\" \"output_view_hidden\" ..." },
-  { "action" : "valueChange", "id" : "clientData (all)", "value" : "List of 4\n $ output_view_hidden   : logi FALSE\n $ output_caption_hidden: logi FALSE\n $ pixelratio           : num 2\n $ output_summary_hidden: logi FALSE" },
-  { "action" : "valueChange", "id" : "clientData$pixelratio", "value" : " num 2" },
-  { "action" : "valueChange", "id" : "names(clientData)", "value" : " chr [1:5] \"output_caption_hidden\" \"output_summary_hidden\" \"output_view_hidden\" ..." },
-  { "action" : "valueChange", "id" : "clientData (all)", "value" : "List of 5\n $ output_view_hidden   : logi FALSE\n $ output_caption_hidden: logi FALSE\n $ pixelratio           : num 2\n $ output_summary_hidden: logi FALSE\n $ url_protocol         : chr \"http:\"" },
-  { "action" : "valueChange", "id" : "clientData$url_protocol", "value" : " chr \"http:\"" },
-  { "action" : "valueChange", "id" : "names(clientData)", "value" : " chr [1:6] \"output_caption_hidden\" \"output_summary_hidden\" \"output_view_hidden\" ..." },
-  { "action" : "valueChange", "id" : "clientData (all)", "value" : "List of 6\n $ output_view_hidden   : logi FALSE\n $ output_caption_hidden: logi FALSE\n $ pixelratio           : num 2\n $ url_hostname         : chr \"localhost\"\n $ output_summary_hidden: logi FALSE\n $ url_protocol         : chr \"http:\"" },
-  { "action" : "valueChange", "id" : "clientData$url_hostname", "value" : " chr \"localhost\"" },
-  { "action" : "valueChange", "id" : "names(clientData)", "value" : " chr [1:7] \"output_caption_hidden\" \"output_summary_hidden\" \"output_view_hidden\" ..." },
-  { "action" : "valueChange", "id" : "clientData (all)", "value" : "List of 7\n $ output_view_hidden   : logi FALSE\n $ url_port             : chr \"8100\"\n $ output_caption_hidden: logi FALSE\n $ pixelratio           : num 2\n $ url_hostname         : chr \"localhost\"\n $ output_summary_hidden: logi FALSE\n $ url_protocol         : chr \"http:\"" },
-  { "action" : "valueChange", "id" : "clientData$url_port", "value" : " chr \"8100\"" },
-  { "action" : "valueChange", "id" : "names(clientData)", "value" : " chr [1:8] \"output_caption_hidden\" \"output_summary_hidden\" \"output_view_hidden\" ..." },
-  { "action" : "valueChange", "id" : "clientData (all)", "value" : "List of 8\n $ url_pathname         : chr \"/\"\n $ output_view_hidden   : logi FALSE\n $ url_port             : chr \"8100\"\n $ output_caption_hidden: logi FALSE\n $ pixelratio           : num 2\n $ url_hostname         : chr \"localhost\"\n $ output_summary_hidden: logi FALSE\n $ url_protocol         : chr \"http:\"" },
-  { "action" : "valueChange", "id" : "clientData$url_pathname", "value" : " chr \"/\"" },
-  { "action" : "valueChange", "id" : "names(clientData)", "value" : " chr [1:9] \"output_caption_hidden\" \"output_summary_hidden\" \"output_view_hidden\" ..." },
-  { "action" : "valueChange", "id" : "clientData (all)", "value" : "List of 9\n $ url_pathname         : chr \"/\"\n $ output_view_hidden   : logi FALSE\n $ url_port             : chr \"8100\"\n $ output_caption_hidden: logi FALSE\n $ pixelratio           : num 2\n $ url_hostname         : chr \"localhost\"\n $ output_summary_hidden: logi FALSE\n $ url_search           : chr \"\"\n $ url_protocol         : chr \"http:\"" },
-  { "action" : "valueChange", "id" : "clientData$url_search", "value" : " chr \"\"" },
-  { "action" : "valueChange", "id" : "names(clientData)", "value" : " chr [1:10] \"output_caption_hidden\" \"output_summary_hidden\" \"output_view_hidden\" ..." },
-  { "action" : "valueChange", "id" : "clientData (all)", "value" : "List of 10\n $ url_pathname         : chr \"/\"\n $ output_view_hidden   : logi FALSE\n $ url_port             : chr \"8100\"\n $ output_caption_hidden: logi FALSE\n $ url_hash_initial     : chr \"\"\n $ pixelratio           : num 2\n $ url_hostname         : chr \"localhost\"\n $ output_summary_hidden: logi FALSE\n $ url_search           : chr \"\"\n $ url_protocol         : chr \"http:\"" },
-  { "action" : "valueChange", "id" : "clientData$url_hash_initial", "value" : " chr \"\"" },
-  { "action" : "valueChange", "id" : "names(clientData)", "value" : " chr [1:11] \"allowDataUriScheme\" \"output_caption_hidden\" \"output_summary_hidden\" ..." },
-  { "action" : "valueChange", "id" : "clientData (all)", "value" : "List of 11\n $ allowDataUriScheme   : logi TRUE\n $ url_pathname         : chr \"/\"\n $ output_view_hidden   : logi FALSE\n $ url_port             : chr \"8100\"\n $ output_caption_hidden: logi FALSE\n $ url_hash_initial     : chr \"\"\n $ pixelratio           : num 2\n $ url_hostname         : chr \"localhost\"\n $ output_summary_hidden: logi FALSE\n $ url_search           : chr \"\"\n $ url_protocol         : chr \"ht [...]
-  { "action" : "valueChange", "id" : "clientData$allowDataUriScheme", "value" : " logi TRUE" },
-  { "action" : "ctx", "id" : "1", "label" : "output$caption <- renderText({ \n    input$caption\n})", "type" : "observer", "prevId" : "" },
-  { "action" : "invalidate", "id" : "1" },
-  { "action" : "ctx", "id" : "2", "label" : "output$summary <- renderPrint({ \n    dataset <- datasetInput()\n    summary(dataset)\n})", "type" : "observer", "prevId" : "" },
-  { "action" : "invalidate", "id" : "2" },
-  { "action" : "ctx", "id" : "3", "label" : "output$view <- renderTable({ \n    head(datasetInput(), n = input$obs)\n})", "type" : "observer", "prevId" : "" },
-  { "action" : "invalidate", "id" : "3" },
-  { "action" : "ctx", "id" : "4", "label" : "output$caption <- renderText({ \n    input$caption\n})", "type" : "observer", "prevId" : "1" },
-  { "action" : "enter", "id" : "4" },
-  { "action" : "dep", "id" : "4", "dependsOn" : "input$caption" },
-  { "action" : "exit", "id" : "4" },
-  { "action" : "ctx", "id" : "5", "label" : "output$summary <- renderPrint({ \n    dataset <- datasetInput()\n    summary(dataset)\n})", "type" : "observer", "prevId" : "2" },
-  { "action" : "enter", "id" : "5" },
-  { "action" : "ctx", "id" : "6", "label" : "reactive({ \n    switch(input$dataset, rock = rock, pressure = pressure, cars = cars)\n})", "type" : "observable", "prevId" : "" },
-  { "action" : "enter", "id" : "6" },
-  { "action" : "dep", "id" : "6", "dependsOn" : "input$dataset" },
-  { "action" : "exit", "id" : "6" },
-  { "action" : "depId", "id" : "5", "dependsOn" : "6" },
-  { "action" : "exit", "id" : "5" },
-  { "action" : "ctx", "id" : "7", "label" : "output$view <- renderTable({ \n    head(datasetInput(), n = input$obs)\n})", "type" : "observer", "prevId" : "3" },
-  { "action" : "enter", "id" : "7" },
-  { "action" : "depId", "id" : "7", "dependsOn" : "6" },
-  { "action" : "dep", "id" : "7", "dependsOn" : "input$obs" },
-  { "action" : "exit", "id" : "7" },
-  { "action" : "valueChange", "id" : "names(input)", "value" : " chr [1:3] \"caption\" \"dataset\" \"obs\"" },
-  { "action" : "valueChange", "id" : "input (all)", "value" : "List of 3\n $ caption: chr \"Pressure Summary\"\n $ obs    : num 10\n $ dataset: chr \"rock\"" },
-  { "action" : "valueChange", "id" : "input$caption", "value" : " chr \"Pressure Summary\"" },
-  { "action" : "invalidate", "id" : "4" },
-  { "action" : "ctx", "id" : "8", "label" : "output$caption <- renderText({ \n    input$caption\n})", "type" : "observer", "prevId" : "4" },
-  { "action" : "enter", "id" : "8" },
-  { "action" : "dep", "id" : "8", "dependsOn" : "input$caption" },
-  { "action" : "exit", "id" : "8" },
-  { "action" : "valueChange", "id" : "names(input)", "value" : " chr [1:3] \"caption\" \"dataset\" \"obs\"" },
-  { "action" : "valueChange", "id" : "input (all)", "value" : "List of 3\n $ caption: chr \"Pressure Summary\"\n $ obs    : num 10\n $ dataset: chr \"pressure\"" },
-  { "action" : "valueChange", "id" : "input$dataset", "value" : " chr \"pressure\"" },
-  { "action" : "invalidate", "id" : "6" },
-  { "action" : "invalidate", "id" : "5" },
-  { "action" : "invalidate", "id" : "7" },
-  { "action" : "ctx", "id" : "9", "label" : "output$summary <- renderPrint({ \n    dataset <- datasetInput()\n    summary(dataset)\n})", "type" : "observer", "prevId" : "5" },
-  { "action" : "enter", "id" : "9" },
-  { "action" : "ctx", "id" : "10", "label" : "reactive({ \n    switch(input$dataset, rock = rock, pressure = pressure, cars = cars)\n})", "type" : "observable", "prevId" : "6" },
-  { "action" : "enter", "id" : "10" },
-  { "action" : "dep", "id" : "10", "dependsOn" : "input$dataset" },
-  { "action" : "exit", "id" : "10" },
-  { "action" : "depId", "id" : "9", "dependsOn" : "10" },
-  { "action" : "exit", "id" : "9" },
-  { "action" : "ctx", "id" : "11", "label" : "output$view <- renderTable({ \n    head(datasetInput(), n = input$obs)\n})", "type" : "observer", "prevId" : "7" },
-  { "action" : "enter", "id" : "11" },
-  { "action" : "depId", "id" : "11", "dependsOn" : "10" },
-  { "action" : "dep", "id" : "11", "dependsOn" : "input$obs" },
-  { "action" : "exit", "id" : "11" },
-  { "action" : "valueChange", "id" : "names(input)", "value" : " chr [1:3] \"caption\" \"dataset\" \"obs\"" },
-  { "action" : "valueChange", "id" : "input (all)", "value" : "List of 3\n $ caption: chr \"Pressure Summary\"\n $ obs    : num 15\n $ dataset: chr \"pressure\"" },
-  { "action" : "valueChange", "id" : "input$obs", "value" : " num 15" },
-  { "action" : "invalidate", "id" : "11" },
-  { "action" : "ctx", "id" : "12", "label" : "output$view <- renderTable({ \n    head(datasetInput(), n = input$obs)\n})", "type" : "observer", "prevId" : "11" },
-  { "action" : "enter", "id" : "12" },
-  { "action" : "depId", "id" : "12", "dependsOn" : "10" },
-  { "action" : "dep", "id" : "12", "dependsOn" : "input$obs" },
-  { "action" : "exit", "id" : "12" }
+  {
+    "action": "valueChange",
+    "id": "names(input)",
+    "value": " chr \"dataset\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9168
+  },
+  {
+    "action": "valueChange",
+    "id": "input (all)",
+    "value": "List of 1\n $ dataset: chr \"rock\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.919
+  },
+  {
+    "action": "valueChange",
+    "id": "input$dataset",
+    "value": " chr \"rock\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9207
+  },
+  {
+    "action": "valueChange",
+    "id": "names(input)",
+    "value": " chr [1:2] \"caption\" \"dataset\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9222
+  },
+  {
+    "action": "valueChange",
+    "id": "input (all)",
+    "value": "List of 2\n $ caption: chr \"Data Summary\"\n $ dataset: chr \"rock\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9243
+  },
+  {
+    "action": "valueChange",
+    "id": "input$caption",
+    "value": " chr \"Data Summary\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9283
+  },
+  {
+    "action": "valueChange",
+    "id": "names(input)",
+    "value": " chr [1:3] \"caption\" \"dataset\" \"obs\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9298
+  },
+  {
+    "action": "valueChange",
+    "id": "input (all)",
+    "value": "List of 3\n $ caption: chr \"Data Summary\"\n $ obs    : int 10\n $ dataset: chr \"rock\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9323
+  },
+  {
+    "action": "valueChange",
+    "id": "input$obs",
+    "value": " int 10",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9339
+  },
+  {
+    "action": "valueChange",
+    "id": "names(clientData)",
+    "value": " chr \"output_caption_hidden\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9355
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData (all)",
+    "value": "List of 1\n $ output_caption_hidden: logi FALSE",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9373
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData$output_caption_hidden",
+    "value": " logi FALSE",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9387
+  },
+  {
+    "action": "valueChange",
+    "id": "names(clientData)",
+    "value": " chr [1:2] \"output_caption_hidden\" \"output_summary_hidden\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9401
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData (all)",
+    "value": "List of 2\n $ output_caption_hidden: logi FALSE\n $ output_summary_hidden: logi FALSE",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9424
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData$output_summary_hidden",
+    "value": " logi FALSE",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.944
+  },
+  {
+    "action": "valueChange",
+    "id": "names(clientData)",
+    "value": " chr [1:3] \"output_caption_hidden\" \"output_summary_hidden\" ...",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9455
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData (all)",
+    "value": "List of 3\n $ output_view_hidden   : logi FALSE\n $ output_caption_hidden: logi FALSE\n $ output_summary_hidden: logi FALSE",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9481
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData$output_view_hidden",
+    "value": " logi FALSE",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9494
+  },
+  {
+    "action": "valueChange",
+    "id": "names(clientData)",
+    "value": " chr [1:4] \"output_caption_hidden\" \"output_summary_hidden\" ...",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9509
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData (all)",
+    "value": "List of 4\n $ output_view_hidden   : logi FALSE\n $ output_caption_hidden: logi FALSE\n $ pixelratio           : int 2\n $ output_summary_hidden: logi FALSE",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.954
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData$pixelratio",
+    "value": " int 2",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9555
+  },
+  {
+    "action": "valueChange",
+    "id": "names(clientData)",
+    "value": " chr [1:5] \"output_caption_hidden\" \"output_summary_hidden\" ...",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9569
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData (all)",
+    "value": "List of 5\n $ output_view_hidden   : logi FALSE\n $ output_caption_hidden: logi FALSE\n $ pixelratio           : int 2\n $ output_summary_hidden: logi FALSE\n $ url_protocol         : chr \"http:\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9605
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData$url_protocol",
+    "value": " chr \"http:\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9619
+  },
+  {
+    "action": "valueChange",
+    "id": "names(clientData)",
+    "value": " chr [1:6] \"output_caption_hidden\" \"output_summary_hidden\" ...",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9633
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData (all)",
+    "value": "List of 6\n $ output_view_hidden   : logi FALSE\n $ output_caption_hidden: logi FALSE\n $ pixelratio           : int 2\n $ url_hostname         : chr \"127.0.0.1\"\n $ output_summary_hidden: logi FALSE\n $ url_protocol         : chr \"http:\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9671
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData$url_hostname",
+    "value": " chr \"127.0.0.1\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9683
+  },
+  {
+    "action": "valueChange",
+    "id": "names(clientData)",
+    "value": " chr [1:7] \"output_caption_hidden\" \"output_summary_hidden\" ...",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9697
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData (all)",
+    "value": "List of 7\n $ output_view_hidden   : logi FALSE\n $ url_port             : chr \"5643\"\n $ output_caption_hidden: logi FALSE\n $ pixelratio           : int 2\n $ url_hostname         : chr \"127.0.0.1\"\n $ output_summary_hidden: logi FALSE\n $ url_protocol         : chr \"http:\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9756
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData$url_port",
+    "value": " chr \"5643\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9769
+  },
+  {
+    "action": "valueChange",
+    "id": "names(clientData)",
+    "value": " chr [1:8] \"output_caption_hidden\" \"output_summary_hidden\" ...",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9784
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData (all)",
+    "value": "List of 8\n $ url_pathname         : chr \"\/\"\n $ output_view_hidden   : logi FALSE\n $ url_port             : chr \"5643\"\n $ output_caption_hidden: logi FALSE\n $ pixelratio           : int 2\n $ url_hostname         : chr \"127.0.0.1\"\n $ output_summary_hidden: logi FALSE\n $ url_protocol         : chr \"http:\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9828
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData$url_pathname",
+    "value": " chr \"\/\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9841
+  },
+  {
+    "action": "valueChange",
+    "id": "names(clientData)",
+    "value": " chr [1:9] \"output_caption_hidden\" \"output_summary_hidden\" ...",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9856
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData (all)",
+    "value": "List of 9\n $ url_pathname         : chr \"\/\"\n $ output_view_hidden   : logi FALSE\n $ url_port             : chr \"5643\"\n $ output_caption_hidden: logi FALSE\n $ pixelratio           : int 2\n $ url_hostname         : chr \"127.0.0.1\"\n $ output_summary_hidden: logi FALSE\n $ url_search           : chr \"\"\n $ url_protocol         : chr \"http:\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9904
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData$url_search",
+    "value": " chr \"\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9918
+  },
+  {
+    "action": "valueChange",
+    "id": "names(clientData)",
+    "value": " chr [1:10] \"output_caption_hidden\" \"output_summary_hidden\" ...",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9934
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData (all)",
+    "value": "List of 10\n $ url_pathname         : chr \"\/\"\n $ output_view_hidden   : logi FALSE\n $ url_port             : chr \"5643\"\n $ output_caption_hidden: logi FALSE\n $ url_hash_initial     : chr \"\"\n $ pixelratio           : int 2\n $ url_hostname         : chr \"127.0.0.1\"\n $ output_summary_hidden: logi FALSE\n $ url_search           : chr \"\"\n $ url_protocol         : chr \"http:\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9985
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData$url_hash_initial",
+    "value": " chr \"\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212774.9998
+  },
+  {
+    "action": "valueChange",
+    "id": "names(clientData)",
+    "value": " chr [1:11] \"output_caption_hidden\" \"output_summary_hidden\" ...",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0015
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData (all)",
+    "value": "List of 11\n $ url_pathname         : chr \"\/\"\n $ output_view_hidden   : logi FALSE\n $ url_port             : chr \"5643\"\n $ output_caption_hidden: logi FALSE\n $ url_hash_initial     : chr \"\"\n $ pixelratio           : int 2\n $ singletons           : chr \"\"\n $ url_hostname         : chr \"127.0.0.1\"\n $ output_summary_hidden: logi FALSE\n $ url_search           : chr \"\"\n $ url_protocol         : chr \"http:\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0077
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData$singletons",
+    "value": " chr \"\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0094
+  },
+  {
+    "action": "valueChange",
+    "id": "names(clientData)",
+    "value": " chr [1:12] \"allowDataUriScheme\" \"output_caption_hidden\" ...",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0113
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData (all)",
+    "value": "List of 12\n $ allowDataUriScheme   : logi TRUE\n $ url_pathname         : chr \"\/\"\n $ output_view_hidden   : logi FALSE\n $ url_port             : chr \"5643\"\n $ output_caption_hidden: logi FALSE\n $ url_hash_initial     : chr \"\"\n $ pixelratio           : int 2\n $ singletons           : chr \"\"\n $ url_hostname         : chr \"127.0.0.1\"\n $ output_summary_hidden: logi FALSE\n $ url_search           : chr \"\"\n $ url_protocol         : chr \"http:\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.018
+  },
+  {
+    "action": "valueChange",
+    "id": "clientData$allowDataUriScheme",
+    "value": " logi TRUE",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0194
+  },
+  {
+    "action": "ctx",
+    "id": "1",
+    "label": "output$caption",
+    "srcref": [
+      34,
+      32,
+      35,
+      17,
+      32,
+      17
+    ],
+    "srcfile": "server.R",
+    "type": "observer",
+    "prevId": "",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0244
+  },
+  {
+    "action": "invalidate",
+    "id": "1",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0259
+  },
+  {
+    "action": "ctx",
+    "id": "2",
+    "label": "output$summary",
+    "srcref": [
+      42,
+      33,
+      44,
+      20,
+      33,
+      20
+    ],
+    "srcfile": "server.R",
+    "type": "observer",
+    "prevId": "",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0286
+  },
+  {
+    "action": "invalidate",
+    "id": "2",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0297
+  },
+  {
+    "action": "ctx",
+    "id": "3",
+    "label": "output$view",
+    "srcref": [
+      50,
+      30,
+      51,
+      39,
+      30,
+      39
+    ],
+    "srcfile": "server.R",
+    "type": "observer",
+    "prevId": "",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0319
+  },
+  {
+    "action": "invalidate",
+    "id": "3",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0331
+  },
+  {
+    "action": "ctx",
+    "id": "4",
+    "label": "output$caption",
+    "srcref": [
+      34,
+      32,
+      35,
+      17,
+      32,
+      17
+    ],
+    "srcfile": "server.R",
+    "type": "observer",
+    "prevId": "1",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0345
+  },
+  {
+    "action": "enter",
+    "id": "4",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0357
+  },
+  {
+    "action": "dep",
+    "id": "4",
+    "dependsOn": "input$caption",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0373
+  },
+  {
+    "action": "exit",
+    "id": "4",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0395
+  },
+  {
+    "action": "ctx",
+    "id": "5",
+    "label": "output$summary",
+    "srcref": [
+      42,
+      33,
+      44,
+      20,
+      33,
+      20
+    ],
+    "srcfile": "server.R",
+    "type": "observer",
+    "prevId": "2",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0405
+  },
+  {
+    "action": "enter",
+    "id": "5",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0416
+  },
+  {
+    "action": "ctx",
+    "id": "6",
+    "label": "datasetInput",
+    "srcref": [
+      16,
+      5,
+      19,
+      25,
+      5,
+      25,
+      16,
+      19
+    ],
+    "srcfile": "server.R",
+    "type": "observable",
+    "prevId": "",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0436
+  },
+  {
+    "action": "enter",
+    "id": "6",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0447
+  },
+  {
+    "action": "dep",
+    "id": "6",
+    "dependsOn": "input$dataset",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0455
+  },
+  {
+    "action": "exit",
+    "id": "6",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0478
+  },
+  {
+    "action": "depId",
+    "id": "5",
+    "dependsOn": "6",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0487
+  },
+  {
+    "action": "exit",
+    "id": "5",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0552
+  },
+  {
+    "action": "ctx",
+    "id": "7",
+    "label": "output$view",
+    "srcref": [
+      50,
+      30,
+      51,
+      39,
+      30,
+      39
+    ],
+    "srcfile": "server.R",
+    "type": "observer",
+    "prevId": "3",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0562
+  },
+  {
+    "action": "enter",
+    "id": "7",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0574
+  },
+  {
+    "action": "depId",
+    "id": "7",
+    "dependsOn": "6",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0589
+  },
+  {
+    "action": "dep",
+    "id": "7",
+    "dependsOn": "input$obs",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.0598
+  },
+  {
+    "action": "exit",
+    "id": "7",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212775.069
+  },
+  {
+    "action": "valueChange",
+    "id": "names(input)",
+    "value": " chr [1:3] \"caption\" \"dataset\" \"obs\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212777.7149
+  },
+  {
+    "action": "valueChange",
+    "id": "input (all)",
+    "value": "List of 3\n $ caption: chr \"Data Summary 1\"\n $ obs    : int 10\n $ dataset: chr \"rock\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212777.7181
+  },
+  {
+    "action": "valueChange",
+    "id": "input$caption",
+    "value": " chr \"Data Summary 1\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212777.7198
+  },
+  {
+    "action": "invalidate",
+    "id": "4",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212777.7208
+  },
+  {
+    "action": "ctx",
+    "id": "8",
+    "label": "output$caption",
+    "srcref": [
+      34,
+      32,
+      35,
+      17,
+      32,
+      17
+    ],
+    "srcfile": "server.R",
+    "type": "observer",
+    "prevId": "4",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212777.7236
+  },
+  {
+    "action": "enter",
+    "id": "8",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212777.7249
+  },
+  {
+    "action": "dep",
+    "id": "8",
+    "dependsOn": "input$caption",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212777.7266
+  },
+  {
+    "action": "exit",
+    "id": "8",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212777.7284
+  },
+  {
+    "action": "valueChange",
+    "id": "names(input)",
+    "value": " chr [1:3] \"caption\" \"dataset\" \"obs\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4006
+  },
+  {
+    "action": "valueChange",
+    "id": "input (all)",
+    "value": "List of 3\n $ caption: chr \"Data Summary 1\"\n $ obs    : int 10\n $ dataset: chr \"pressure\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4035
+  },
+  {
+    "action": "valueChange",
+    "id": "input$dataset",
+    "value": " chr \"pressure\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4049
+  },
+  {
+    "action": "invalidate",
+    "id": "6",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4058
+  },
+  {
+    "action": "invalidate",
+    "id": "5",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4065
+  },
+  {
+    "action": "invalidate",
+    "id": "7",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4089
+  },
+  {
+    "action": "ctx",
+    "id": "9",
+    "label": "output$summary",
+    "srcref": [
+      42,
+      33,
+      44,
+      20,
+      33,
+      20
+    ],
+    "srcfile": "server.R",
+    "type": "observer",
+    "prevId": "5",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4109
+  },
+  {
+    "action": "enter",
+    "id": "9",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.412
+  },
+  {
+    "action": "ctx",
+    "id": "10",
+    "label": "datasetInput",
+    "srcref": [
+      16,
+      5,
+      19,
+      25,
+      5,
+      25,
+      16,
+      19
+    ],
+    "srcfile": "server.R",
+    "type": "observable",
+    "prevId": "6",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4142
+  },
+  {
+    "action": "enter",
+    "id": "10",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4153
+  },
+  {
+    "action": "dep",
+    "id": "10",
+    "dependsOn": "input$dataset",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4161
+  },
+  {
+    "action": "exit",
+    "id": "10",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4169
+  },
+  {
+    "action": "depId",
+    "id": "9",
+    "dependsOn": "10",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4177
+  },
+  {
+    "action": "exit",
+    "id": "9",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4777
+  },
+  {
+    "action": "ctx",
+    "id": "11",
+    "label": "output$view",
+    "srcref": [
+      50,
+      30,
+      51,
+      39,
+      30,
+      39
+    ],
+    "srcfile": "server.R",
+    "type": "observer",
+    "prevId": "7",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4789
+  },
+  {
+    "action": "enter",
+    "id": "11",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.48
+  },
+  {
+    "action": "depId",
+    "id": "11",
+    "dependsOn": "10",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4818
+  },
+  {
+    "action": "dep",
+    "id": "11",
+    "dependsOn": "input$obs",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4827
+  },
+  {
+    "action": "exit",
+    "id": "11",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212779.4903
+  },
+  {
+    "action": "valueChange",
+    "id": "names(input)",
+    "value": " chr [1:3] \"caption\" \"dataset\" \"obs\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212781.0019
+  },
+  {
+    "action": "valueChange",
+    "id": "input (all)",
+    "value": "List of 3\n $ caption: chr \"Data Summary 1\"\n $ obs    : int 100\n $ dataset: chr \"pressure\"",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212781.0052
+  },
+  {
+    "action": "valueChange",
+    "id": "input$obs",
+    "value": " int 100",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212781.0069
+  },
+  {
+    "action": "invalidate",
+    "id": "11",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212781.008
+  },
+  {
+    "action": "ctx",
+    "id": "12",
+    "label": "output$view",
+    "srcref": [
+      50,
+      30,
+      51,
+      39,
+      30,
+      39
+    ],
+    "srcfile": "server.R",
+    "type": "observer",
+    "prevId": "11",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212781.0119
+  },
+  {
+    "action": "enter",
+    "id": "12",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212781.0132
+  },
+  {
+    "action": "depId",
+    "id": "12",
+    "dependsOn": "10",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212781.0155
+  },
+  {
+    "action": "dep",
+    "id": "12",
+    "dependsOn": "input$obs",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212781.0172
+  },
+  {
+    "action": "exit",
+    "id": "12",
+    "session": "36377b30e77d6f2f1f5fc5628b882ee5",
+    "time": 1449212781.0257
+  }
 ];
+var time = true;
 try {
   log = __DATA__;
+  time = String(__TIME__).toLowerCase() === 'true';
 } catch (e) {}
 
 var nodes = {};
@@ -302,29 +1050,31 @@ function getTargetCoords(node) {
   }
 }
 
-function multilineTextNode(node) {
-  var MAX_LINES = 6;
-  var fade = false;
-  var el = d3.select(this);
-  var lines = el.text().split('\n');
-  if (lines.length > MAX_LINES) {
-    lines.splice(MAX_LINES);
-    fade = true;
-  }
-  el.text('');
-  var tspan = el.selectAll('tspan').data(lines);
-  tspan.enter().append('tspan');
-  tspan
-      .attr('x', 8)
-      .attr('dy', function(line, i) { return i > 0 ? '1em' : 0})
-      .attr('opacity', function(line, i) {
-        if (!fade)
-          return 1;
-        return Math.min(1, (MAX_LINES - i) * 0.25 - 0.15);
-      })
-      .text(function(line) { return line; });
+function compare(a, b) {
+  if (a.id < b.id)
+    return -1;
+  else if (a.id > b.id)
+    return 1;
+  else
+    return 0;
 }
 
+var timeDifferences = function() {
+  var enters = log.filter(function(obj) { return obj.action === 'enter' });
+  enters.sort(compare);
+  var exits = log.filter(function(obj) { return obj.action === 'exit' });
+  exits.sort(compare);
+  var diff = [];
+  for (var i = 0; i < exits.length; i++)
+    diff.push(exits[i].time - enters[i].time);
+  return diff.map(function(x) { return x * 1000; });
+}();
+
+// colors taken from colorbrewer's 9-class Reds: http://colorbrewer2.org/?type=sequential&scheme=Reds&n=9
+var colorScale = d3.scale.quantize()
+                         .domain([d3.min(timeDifferences), d3.max(timeDifferences)])
+                         .range(['#fcbba1','#fc9272','#fb6a4a','#ef3b2c','#cb181d','#a50f15']);
+
 function update() {
   force.size([document.documentElement.clientWidth / 4,
               document.documentElement.clientHeight / 4]);
@@ -345,9 +1095,15 @@ function update() {
         $('#description').text(n.label);
       })
       .on('mouseout', function(d, i) {
-        $('#description').html('');
+        $('#description').text('');
+      })
+      .on('dblclick', function(d) {
+        d3.event.stopPropagation();
+        d3.select(this).classed('fixed', d.fixed = false);
       })
-      .call(force.drag);
+      .call(force.drag().on('dragstart', function(d) {
+        d3.select(this).classed('fixed', d.fixed = true);
+      }));
   newG.append('path')
       .attr('transform', 'scale(0.08)')
       .attr('stroke', 'black')
@@ -356,7 +1112,7 @@ function update() {
       .attr('d', pathDataForNode);
   newG.append('text')
       .attr('x', 3)
-      .attr('y', 0)
+      .attr('y', 1.5)
       .attr('font-size', 3.25)
       .attr('transform', function(n) {
         if (n.type !== 'observer')
@@ -375,13 +1131,13 @@ function update() {
         else
           return null;
       });
-  var tspan = node.selectAll('text').filter(function(n) {
+  var tspanLabel = node.selectAll('text').filter(function(n) {
     // This filter is used to disregard all nodes whose labels have
     // not changed since the last time we updated them.
     var changed = n.label !== this.label;
     this.label = n.label;
     return changed;
-  }).selectAll('tspan')
+  }).selectAll('.tspanLabel')
     .data(function(n) {
       var lines = n.label.replace(/ /g, '\xA0').split('\n');
       if (lines.length > MAX_LINES) {
@@ -389,16 +1145,37 @@ function update() {
       }
       return lines;
     });
-  tspan.enter().append('tspan');
-  tspan.exit().remove();
-  tspan
+  tspanLabel.enter().append('tspan');
+  tspanLabel.exit().remove();
+  tspanLabel
       .attr('x', 8)
       .attr('dy', function(line, i) { return i > 0 ? '1em' : 0})
       .attr('opacity', function(line, i) {
         return Math.min(1, (MAX_LINES - i) * 0.25 - 0.15);
       })
+      .classed('tspanLabel', true)
       .text(function(line) { return line; });
 
+  var tspanTime = node.selectAll('text').filter(function(n) {
+    var changed = n.timeElapsed !== this.timeElapsed;
+    this.timeElapsed = n.timeElapsed;
+    return changed;
+  }).selectAll('.tspanTime')
+    .data(function(n) {
+      return [n.timeElapsed];
+    });
+  tspanTime.enter().append('tspan');
+  tspanTime.exit().remove();
+  tspanTime
+      .attr('x', 8)
+      .attr('y', -2)
+      .attr('fill', function(time) { return colorScale(time); })
+      .classed('tspanTime', true)
+      .text(function(time) {
+        if (time === null) return '';
+        else return 'time elapsed: ' + (Math.round(time * 1000) / 1000) + ' ms';
+      });
+
   link = d3.select('#links').selectAll('.link').data(links);
   layoutDirty = layoutDirty || !link.enter().empty() || !link.exit().empty();
   link.enter().append('path')
@@ -551,7 +1328,11 @@ var callbacks = {
   dep: function(data) {
     var dependsOn = nodes[data.dependsOn];
     if (!dependsOn) {
-      createNodeWithUndo({id: data.dependsOn, label: data.dependsOn, type: 'value'});
+      createNodeWithUndo({
+        id: data.dependsOn,
+        label: data.dependsOn,
+        type: 'value'
+      });
       dependsOn = nodes[data.dependsOn];
     }
     if (dependsOn.hide) {
@@ -622,21 +1403,28 @@ var callbacks = {
   enter: function(data) {
     var node = nodes[data.id];
     node.running = true;
+    node.start = data.time;
+    var oldTimeElapsed = node.timeElapsed;
+    node.timeElapsed = null;
     pushUndo(function() {
       node.running = false;
+      node.start = null;
+      node.timeElapsed = oldTimeElapsed;
     });
   },
   exit: function(data) {
     var node = nodes[data.id];
     node.running = false;
+    var oldTimeElapsed = node.timeElapsed;
+    node.timeElapsed = time ? (parseFloat(data.time) - parseFloat(node.start)) * 1000 : null;
     pushUndo(function() {
       node.running = true;
+      node.timeElapsed = oldTimeElapsed;
     });
   }
 };
 
 function processMessage(data, suppressUpdate) {
-  console.log(JSON.stringify(data));
   if (!callbacks.hasOwnProperty(data.action))
     throw new Error('Unknown action ' + data.action);
   var result = callbacks[data.action].call(callbacks, data);
@@ -777,9 +1565,9 @@ $(function() {
   <svg>
     <defs>
       <marker id="triangle"
-              viewBox="0 0 10 10" 
+              viewBox="0 0 10 10"
               refX="5" refY="5"
-              markerWidth="6" 
+              markerWidth="6"
               markerHeight="6"
               orient="auto">
           <path d="M 10 0 L 0 5 L 10 10 z" />
diff --git a/inst/www/shared/babel-polyfill.min.js b/inst/www/shared/babel-polyfill.min.js
new file mode 100644
index 0000000..6fecdd8
--- /dev/null
+++ b/inst/www/shared/babel-polyfill.min.js
@@ -0,0 +1,3 @@
+!function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var c="function"==typeof require&&require;if(!u&&c)return c(o,!0);if(i)return i(o,!0);var a=new Error("Cannot find module '"+o+"'");throw a.code="MODULE_NOT_FOUND",a}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(n){var r=t[o][1][n];return s(r?r:n)},f,f.exports,e,t,n,r)}return n[o].exports}for(var i="function"==typeof require&&require,o=0;o<r.length;o++)s(r[o]);return s}({1:[function(t,n,r){(function(n){"use strict";function [...]
+},indexOf:function indexOf(t){return ut(Nt(this),t,arguments.length>1?arguments[1]:void 0)},includes:function includes(t){return ot(Nt(this),t,arguments.length>1?arguments[1]:void 0)},join:function join(t){return vt.apply(Nt(this),arguments)},lastIndexOf:function lastIndexOf(t){return st.apply(Nt(this),arguments)},map:function map(t){return Ft(Nt(this),t,arguments.length>1?arguments[1]:void 0)},reduce:function reduce(t){return lt.apply(Nt(this),arguments)},reduceRight:function reduceRigh [...]
+c(c.S,"Reflect",{set:set})},{11:11,33:33,40:40,50:50,68:68,70:70,74:74,85:85}],211:[function(t,n,r){var e=t(39),i=t(44),o=t(68).f,u=t(72).f,c=t(51),a=t(37),f=e.RegExp,s=f,l=f.prototype,h=/a/g,v=/a/g,p=new f(h)!==h;if(t(29)&&(!p||t(35)(function(){return v[t(115)("match")]=!1,f(h)!=h||f(v)==v||"/a/i"!=f(h,"i")}))){f=function RegExp(t,n){var r=this instanceof f,e=c(t),o=void 0===n;return!r&&e&&t.constructor===f&&o?t:i(p?new s(e&&!o?t.source:t,n):s((e=t instanceof f)?t.source:t,e&&o?a.call(t [...]
diff --git a/inst/www/shared/highlight/rstudio.css b/inst/www/shared/highlight/rstudio.css
index 2be0a1c..58de9a9 100644
--- a/inst/www/shared/highlight/rstudio.css
+++ b/inst/www/shared/highlight/rstudio.css
@@ -1,3 +1,7 @@
+code {
+  line-height: 150%;
+}
+
 pre .operator,
 pre .paren {
   color: rgb(104, 118, 135)
@@ -13,9 +17,11 @@ pre .number {
 
 pre .comment {
   color: rgb(76, 136, 107);
+  font-style: italic;
 }
 
-pre .keyword {
+pre .keyword,
+pre .id {
   color: rgb(0, 0, 255);
 }
 
@@ -23,7 +29,53 @@ pre .identifier {
   color: rgb(0, 0, 0);
 }
 
-pre .string {
+pre .string,
+pre .attribute {
   color: rgb(3, 106, 7);
 }
 
+pre .doctype {
+  color: rgb(104, 104, 92);
+}
+
+pre .tag,
+pre .title {
+  color: rgb(4, 29, 140);
+}
+
+pre .value {
+  color: rgb(13, 105, 18);
+}
+
+.language-xml .attribute {
+  color: rgb(0, 0, 0);
+}
+
+.language-css .attribute {
+  color: rgb(110, 124, 219);
+}
+
+.language-css .value {
+  color: rgb(23, 149, 30);
+}
+
+.language-css .number,
+.language-css .hexcolor {
+  color: rgb(7, 27, 201);
+}
+
+.language-css .function {
+  color: rgb(61, 77, 113);
+}
+
+.language-css .tag {
+  color: rgb(195, 13, 25);
+}
+
+.language-css .class {
+  color: rgb(53, 132, 148);
+}
+
+.language-css .pseudo {
+  color: rgb(13, 105, 18);
+}
diff --git a/inst/www/shared/ionrangeslider/js/ion.rangeSlider.js b/inst/www/shared/ionrangeslider/js/ion.rangeSlider.js
index 81a4660..d8a36b4 100644
--- a/inst/www/shared/ionrangeslider/js/ion.rangeSlider.js
+++ b/inst/www/shared/ionrangeslider/js/ion.rangeSlider.js
@@ -1,5 +1,5 @@
 // Ion.RangeSlider
-// version 2.0.12 Build: 331
+// version 2.1.2 Build: 350
 // © Denis Ineshin, 2015
 // https://github.com/IonDen
 //
@@ -137,8 +137,16 @@
     // =================================================================================================================
     // Core
 
+    /**
+     * Main plugin constructor
+     *
+     * @param input {Object} link to base input element
+     * @param options {Object} slider config
+     * @param plugin_count {Number}
+     * @constructor
+     */
     var IonRangeSlider = function (input, options, plugin_count) {
-        this.VERSION = "2.0.12";
+        this.VERSION = "2.1.2";
         this.input = input;
         this.plugin_count = plugin_count;
         this.current_plugin = 0;
@@ -146,16 +154,20 @@
         this.update_tm = 0;
         this.old_from = 0;
         this.old_to = 0;
+        this.old_min_interval = null;
         this.raf_id = null;
         this.dragging = false;
         this.force_redraw = false;
+        this.no_diapason = false;
         this.is_key = false;
         this.is_update = false;
         this.is_start = true;
+        this.is_finish = false;
         this.is_active = false;
         this.is_resize = false;
         this.is_click = false;
 
+        // cache for links to all DOM elements
         this.$cache = {
             win: $(window),
             body: $(document.body),
@@ -180,9 +192,135 @@
             grid_labels: []
         };
 
-        // get config data attributes
-        var $inp = this.$cache.input;
-        var data = {
+        // storage for measure variables
+        this.coords = {
+            // left
+            x_gap: 0,
+            x_pointer: 0,
+
+            // width
+            w_rs: 0,
+            w_rs_old: 0,
+            w_handle: 0,
+
+            // percents
+            p_gap: 0,
+            p_gap_left: 0,
+            p_gap_right: 0,
+            p_step: 0,
+            p_pointer: 0,
+            p_handle: 0,
+            p_single_fake: 0,
+            p_single_real: 0,
+            p_from_fake: 0,
+            p_from_real: 0,
+            p_to_fake: 0,
+            p_to_real: 0,
+            p_bar_x: 0,
+            p_bar_w: 0,
+
+            // grid
+            grid_gap: 0,
+            big_num: 0,
+            big: [],
+            big_w: [],
+            big_p: [],
+            big_x: []
+        };
+
+        // storage for labels measure variables
+        this.labels = {
+            // width
+            w_min: 0,
+            w_max: 0,
+            w_from: 0,
+            w_to: 0,
+            w_single: 0,
+
+            // percents
+            p_min: 0,
+            p_max: 0,
+            p_from_fake: 0,
+            p_from_left: 0,
+            p_to_fake: 0,
+            p_to_left: 0,
+            p_single_fake: 0,
+            p_single_left: 0
+        };
+
+
+
+        /**
+         * get and validate config
+         */
+        var $inp = this.$cache.input,
+            val = $inp.prop("value"),
+            config, config_from_data, prop;
+
+        // default config
+        config = {
+            type: "single",
+
+            min: 10,
+            max: 100,
+            from: null,
+            to: null,
+            step: 1,
+
+            min_interval: 0,
+            max_interval: 0,
+            drag_interval: false,
+
+            values: [],
+            p_values: [],
+
+            from_fixed: false,
+            from_min: null,
+            from_max: null,
+            from_shadow: false,
+
+            to_fixed: false,
+            to_min: null,
+            to_max: null,
+            to_shadow: false,
+
+            prettify_enabled: true,
+            prettify_separator: " ",
+            prettify: null,
+
+            force_edges: false,
+
+            keyboard: false,
+            keyboard_step: 5,
+
+            grid: false,
+            grid_margin: true,
+            grid_num: 4,
+            grid_snap: false,
+
+            hide_min_max: false,
+            hide_from_to: false,
+
+            prefix: "",
+            postfix: "",
+            max_postfix: "",
+            decorate_both: true,
+            values_separator: " — ",
+
+            input_values_separator: ";",
+
+            disable: false,
+
+            onStart: null,
+            onChange: null,
+            onFinish: null,
+            onUpdate: null
+        };
+
+
+
+        // config from data-attributes extends js config
+        config_from_data = {
             type: $inp.data("type"),
 
             min: $inp.data("min"),
@@ -229,14 +367,25 @@
             decorate_both: $inp.data("decorateBoth"),
             values_separator: $inp.data("valuesSeparator"),
 
+            input_values_separator: $inp.data("inputValuesSeparator"),
+
             disable: $inp.data("disable")
         };
-        data.values = data.values && data.values.split(",");
+        config_from_data.values = config_from_data.values && config_from_data.values.split(",");
+
+        for (prop in config_from_data) {
+            if (config_from_data.hasOwnProperty(prop)) {
+                if (!config_from_data[prop] && config_from_data[prop] !== 0) {
+                    delete config_from_data[prop];
+                }
+            }
+        }
+
 
-        // get from and to out of input
-        var val = $inp.prop("value");
+
+        // input value extends default config
         if (val) {
-            val = val.split(";");
+            val = val.split(config_from_data.input_values_separator || options.input_values_separator || ";");
 
             if (val[0] && val[0] == +val[0]) {
                 val[0] = +val[0];
@@ -246,77 +395,32 @@
             }
 
             if (options && options.values && options.values.length) {
-                data.from = val[0] && options.values.indexOf(val[0]);
-                data.to = val[1] && options.values.indexOf(val[1]);
+                config.from = val[0] && options.values.indexOf(val[0]);
+                config.to = val[1] && options.values.indexOf(val[1]);
             } else {
-                data.from = val[0] && +val[0];
-                data.to = val[1] && +val[1];
+                config.from = val[0] && +val[0];
+                config.to = val[1] && +val[1];
             }
         }
 
-        // JS config has a priority
-        options = $.extend(data, options);
 
-        // get config from options
-        this.options = $.extend({
-            type: "single",
 
-            min: 10,
-            max: 100,
-            from: null,
-            to: null,
-            step: 1,
+        // js config extends default config
+        $.extend(config, options);
 
-            min_interval: 0,
-            max_interval: 0,
-            drag_interval: false,
 
-            values: [],
-            p_values: [],
+        // data config extends config
+        $.extend(config, config_from_data);
+        this.options = config;
 
-            from_fixed: false,
-            from_min: null,
-            from_max: null,
-            from_shadow: false,
 
-            to_fixed: false,
-            to_min: null,
-            to_max: null,
-            to_shadow: false,
-
-            prettify_enabled: true,
-            prettify_separator: " ",
-            prettify: null,
-
-            force_edges: false,
-
-            keyboard: false,
-            keyboard_step: 5,
 
-            grid: false,
-            grid_margin: true,
-            grid_num: 4,
-            grid_snap: false,
-
-            hide_min_max: false,
-            hide_from_to: false,
-
-            prefix: "",
-            postfix: "",
-            max_postfix: "",
-            decorate_both: true,
-            values_separator: " — ",
+        // validate config, to be sure that all data types are correct
+        this.validate();
 
-            disable: false,
 
-            onStart: null,
-            onChange: null,
-            onFinish: null,
-            onUpdate: null
-        }, options);
-
-        this.validate();
 
+        // default result object, returned to callbacks
         this.result = {
             input: this.$cache.input,
             slider: null,
@@ -333,66 +437,22 @@
             to_value: null
         };
 
-        this.coords = {
-            // left
-            x_gap: 0,
-            x_pointer: 0,
 
-            // width
-            w_rs: 0,
-            w_rs_old: 0,
-            w_handle: 0,
-
-            // percents
-            p_gap: 0,
-            p_gap_left: 0,
-            p_gap_right: 0,
-            p_step: 0,
-            p_pointer: 0,
-            p_handle: 0,
-            p_single: 0,
-            p_single_real: 0,
-            p_from: 0,
-            p_from_real: 0,
-            p_to: 0,
-            p_to_real: 0,
-            p_bar_x: 0,
-            p_bar_w: 0,
-
-            // grid
-            grid_gap: 0,
-            big_num: 0,
-            big: [],
-            big_w: [],
-            big_p: [],
-            big_x: []
-        };
-
-        this.labels = {
-            // width
-            w_min: 0,
-            w_max: 0,
-            w_from: 0,
-            w_to: 0,
-            w_single: 0,
-
-            // percents
-            p_min: 0,
-            p_max: 0,
-            p_from: 0,
-            p_from_left: 0,
-            p_to: 0,
-            p_to_left: 0,
-            p_single: 0,
-            p_single_left: 0
-        };
 
         this.init();
     };
 
     IonRangeSlider.prototype = {
+
+        /**
+         * Starts or updates the plugin instance
+         *
+         * @param is_update {boolean}
+         */
         init: function (is_update) {
-            this.coords.p_step = this.options.step / ((this.options.max - this.options.min) / 100);
+            this.no_diapason = false;
+            this.coords.p_step = this.convertToPercent(this.options.step, true);
+
             this.target = "base";
 
             this.toggleInput();
@@ -403,21 +463,22 @@
                 this.force_redraw = true;
                 this.calc(true);
 
-                if (this.options.onUpdate && typeof this.options.onUpdate === "function") {
-                    this.options.onUpdate(this.result);
-                }
+                // callbacks called
+                this.callOnUpdate();
             } else {
                 this.force_redraw = true;
                 this.calc(true);
 
-                if (this.options.onStart && typeof this.options.onStart === "function") {
-                    this.options.onStart(this.result);
-                }
+                // callbacks called
+                this.callOnStart();
             }
 
             this.updateScene();
         },
 
+        /**
+         * Appends slider template to a DOM
+         */
         append: function () {
             var container_html = '<span class="irs js-irs-' + this.plugin_count + '"></span>';
             this.$cache.input.before(container_html);
@@ -469,8 +530,16 @@
                 this.$cache.input[0].disabled = false;
                 this.bindEvents();
             }
+
+            if (this.options.drag_interval) {
+                this.$cache.bar[0].style.cursor = "ew-resize";
+            }
         },
 
+        /**
+         * Determine which handler has a priority
+         * works only for double slider type
+         */
         setTopHandler: function () {
             var min = this.options.min,
                 max = this.options.max,
@@ -484,11 +553,51 @@
             }
         },
 
+        /**
+         * Determine which handles was clicked last
+         * and which handler should have hover effect
+         *
+         * @param target {String}
+         */
+        changeLevel: function (target) {
+            switch (target) {
+                case "single":
+                    this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_single_fake);
+                    break;
+                case "from":
+                    this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_from_fake);
+                    this.$cache.s_from.addClass("state_hover");
+                    this.$cache.s_from.addClass("type_last");
+                    this.$cache.s_to.removeClass("type_last");
+                    break;
+                case "to":
+                    this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_to_fake);
+                    this.$cache.s_to.addClass("state_hover");
+                    this.$cache.s_to.addClass("type_last");
+                    this.$cache.s_from.removeClass("type_last");
+                    break;
+                case "both":
+                    this.coords.p_gap_left = this.toFixed(this.coords.p_pointer - this.coords.p_from_fake);
+                    this.coords.p_gap_right = this.toFixed(this.coords.p_to_fake - this.coords.p_pointer);
+                    this.$cache.s_to.removeClass("type_last");
+                    this.$cache.s_from.removeClass("type_last");
+                    break;
+            }
+        },
+
+        /**
+         * Then slider is disabled
+         * appends extra layer with opacity
+         */
         appendDisableMask: function () {
             this.$cache.cont.append(disable_html);
             this.$cache.cont.addClass("irs-disabled");
         },
 
+        /**
+         * Remove slider instance
+         * and ubind all events
+         */
         remove: function () {
             this.$cache.cont.remove();
             this.$cache.cont = null;
@@ -515,7 +624,14 @@
             cancelAnimationFrame(this.raf_id);
         },
 
+        /**
+         * bind all slider events
+         */
         bindEvents: function () {
+            if (this.no_diapason) {
+                return;
+            }
+
             this.$cache.body.on("touchmove.irs_" + this.plugin_count, this.pointerMove.bind(this));
             this.$cache.body.on("mousemove.irs_" + this.plugin_count, this.pointerMove.bind(this));
 
@@ -534,19 +650,28 @@
             }
 
             if (this.options.type === "single") {
+                this.$cache.single.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "single"));
                 this.$cache.s_single.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "single"));
                 this.$cache.shad_single.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click"));
 
+                this.$cache.single.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "single"));
                 this.$cache.s_single.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "single"));
                 this.$cache.edge.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click"));
                 this.$cache.shad_single.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click"));
             } else {
+                this.$cache.single.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, null));
+                this.$cache.single.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, null));
+
+                this.$cache.from.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "from"));
                 this.$cache.s_from.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "from"));
+                this.$cache.to.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "to"));
                 this.$cache.s_to.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "to"));
                 this.$cache.shad_from.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click"));
                 this.$cache.shad_to.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click"));
 
+                this.$cache.from.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "from"));
                 this.$cache.s_from.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "from"));
+                this.$cache.to.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "to"));
                 this.$cache.s_to.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "to"));
                 this.$cache.shad_from.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click"));
                 this.$cache.shad_to.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click"));
@@ -562,6 +687,12 @@
             }
         },
 
+        /**
+         * Mousemove or touchmove
+         * only for handlers
+         *
+         * @param e {Object} event object
+         */
         pointerMove: function (e) {
             if (!this.dragging) {
                 return;
@@ -573,6 +704,12 @@
             this.calc();
         },
 
+        /**
+         * Mouseup or touchend
+         * only for handlers
+         *
+         * @param e {Object} event object
+         */
         pointerUp: function (e) {
             if (this.current_plugin !== this.plugin_count) {
                 return;
@@ -584,51 +721,33 @@
                 return;
             }
 
-            var is_function = this.options.onFinish && typeof this.options.onFinish === "function",
-                is_original = $.contains(this.$cache.cont[0], e.target) || this.dragging;
-
-            if (is_function && is_original) {
-                this.options.onFinish(this.result);
-            }
-
             this.$cache.cont.find(".state_hover").removeClass("state_hover");
 
             this.force_redraw = true;
-            this.dragging = false;
 
             if (is_old_ie) {
                 $("*").prop("unselectable", false);
             }
 
             this.updateScene();
-        },
+            this.restoreOriginalMinInterval();
 
-        changeLevel: function (target) {
-            switch (target) {
-                case "single":
-                    this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_single);
-                    break;
-                case "from":
-                    this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_from);
-                    this.$cache.s_from.addClass("state_hover");
-                    this.$cache.s_from.addClass("type_last");
-                    this.$cache.s_to.removeClass("type_last");
-                    break;
-                case "to":
-                    this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_to);
-                    this.$cache.s_to.addClass("state_hover");
-                    this.$cache.s_to.addClass("type_last");
-                    this.$cache.s_from.removeClass("type_last");
-                    break;
-                case "both":
-                    this.coords.p_gap_left = this.toFixed(this.coords.p_pointer - this.coords.p_from);
-                    this.coords.p_gap_right = this.toFixed(this.coords.p_to - this.coords.p_pointer);
-                    this.$cache.s_to.removeClass("type_last");
-                    this.$cache.s_from.removeClass("type_last");
-                    break;
+            // callbacks call
+            if ($.contains(this.$cache.cont[0], e.target) || this.dragging) {
+                this.is_finish = true;
+                this.callOnFinish();
             }
+            
+            this.dragging = false;
         },
 
+        /**
+         * Mousedown or touchstart
+         * only for handlers
+         *
+         * @param target {String|null}
+         * @param e {Object} event object
+         */
         pointerDown: function (target, e) {
             e.preventDefault();
             e.stopPropagation();
@@ -637,6 +756,14 @@
                 return;
             }
 
+            if (target === "both") {
+                this.setTempMinInterval();
+            }
+
+            if (!target) {
+                target = this.target;
+            }
+
             this.current_plugin = this.plugin_count;
             this.target = target;
 
@@ -646,7 +773,7 @@
             this.coords.x_gap = this.$cache.rs.offset().left;
             this.coords.x_pointer = x - this.coords.x_gap;
 
-            this.calcPointer();
+            this.calcPointerPercent();
             this.changeLevel(target);
 
             if (is_old_ie) {
@@ -658,6 +785,13 @@
             this.updateScene();
         },
 
+        /**
+         * Mousedown or touchstart
+         * for other slider elements, like diapason line
+         *
+         * @param target {String}
+         * @param e {Object} event object
+         */
         pointerClick: function (target, e) {
             e.preventDefault();
             e.stopPropagation();
@@ -679,6 +813,13 @@
             this.$cache.line.trigger("focus");
         },
 
+        /**
+         * Keyborard controls for focused slider
+         *
+         * @param target {String}
+         * @param e {Object} event object
+         * @returns {boolean|undefined}
+         */
         key: function (target, e) {
             if (this.current_plugin !== this.plugin_count || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
                 return;
@@ -705,8 +846,12 @@
             return true;
         },
 
-        // Move by key. Beta
-        // TODO: refactor than have plenty of time
+        /**
+         * Move by key. Beta
+         * @todo refactor than have plenty of time
+         *
+         * @param right {boolean} direction to move
+         */
         moveByKey: function (right) {
             var p = this.coords.p_pointer;
 
@@ -721,6 +866,10 @@
             this.calc();
         },
 
+        /**
+         * Set visibility and content
+         * of Min and Max labels
+         */
         setMinMax: function () {
             if (!this.options) {
                 return;
@@ -744,11 +893,40 @@
             this.labels.w_max = this.$cache.max.outerWidth(false);
         },
 
+        /**
+         * Then dragging interval, prevent interval collapsing
+         * using min_interval option
+         */
+        setTempMinInterval: function () {
+            var interval = this.result.to - this.result.from;
+
+            if (this.old_min_interval === null) {
+                this.old_min_interval = this.options.min_interval;
+            }
+
+            this.options.min_interval = interval;
+        },
+
+        /**
+         * Restore min_interval option to original
+         */
+        restoreOriginalMinInterval: function () {
+            if (this.old_min_interval !== null) {
+                this.options.min_interval = this.old_min_interval;
+                this.old_min_interval = null;
+            }
+        },
+
 
 
         // =============================================================================================================
         // Calculations
 
+        /**
+         * All calculations and measures start here
+         *
+         * @param update {boolean=}
+         */
         calc: function (update) {
             if (!this.options) {
                 return;
@@ -759,33 +937,26 @@
             if (this.calc_count === 10 || update) {
                 this.calc_count = 0;
                 this.coords.w_rs = this.$cache.rs.outerWidth(false);
-                if (this.options.type === "single") {
-                    this.coords.w_handle = this.$cache.s_single.outerWidth(false);
-                } else {
-                    this.coords.w_handle = this.$cache.s_from.outerWidth(false);
-                }
+
+                this.calcHandlePercent();
             }
 
             if (!this.coords.w_rs) {
                 return;
             }
 
-            this.calcPointer();
-
-            this.coords.p_handle = this.toFixed(this.coords.w_handle / this.coords.w_rs * 100);
-            var real_width = 100 - this.coords.p_handle,
-                real_x = this.toFixed(this.coords.p_pointer - this.coords.p_gap);
+            this.calcPointerPercent();
+            var handle_x = this.getHandleX();
 
             if (this.target === "click") {
                 this.coords.p_gap = this.coords.p_handle / 2;
-                real_x = this.toFixed(this.coords.p_pointer - this.coords.p_gap);
-                this.target = this.chooseHandle(real_x);
-            }
+                handle_x = this.getHandleX();
 
-            if (real_x < 0) {
-                real_x = 0;
-            } else if (real_x > real_width) {
-                real_x = real_width;
+                if (this.options.drag_interval) {
+                    this.target = "both_one";
+                } else {
+                    this.target = this.chooseHandle(handle_x);
+                }
             }
 
             switch (this.target) {
@@ -802,9 +973,9 @@
                     this.coords.p_from_real = this.checkDiapason(this.coords.p_from_real, this.options.from_min, this.options.from_max);
                     this.coords.p_to_real = this.checkDiapason(this.coords.p_to_real, this.options.to_min, this.options.to_max);
 
-                    this.coords.p_single = this.toFixed(f - (this.coords.p_handle / 100 * f));
-                    this.coords.p_from = this.toFixed(f - (this.coords.p_handle / 100 * f));
-                    this.coords.p_to = this.toFixed(t - (this.coords.p_handle / 100 * t));
+                    this.coords.p_single_fake = this.convertToFakePercent(this.coords.p_single_real);
+                    this.coords.p_from_fake = this.convertToFakePercent(this.coords.p_from_real);
+                    this.coords.p_to_fake = this.convertToFakePercent(this.coords.p_to_real);
 
                     this.target = null;
 
@@ -815,9 +986,11 @@
                         break;
                     }
 
-                    this.coords.p_single_real = this.calcWithStep(real_x / real_width * 100);
+                    this.coords.p_single_real = this.convertToRealPercent(handle_x);
+                    this.coords.p_single_real = this.calcWithStep(this.coords.p_single_real);
                     this.coords.p_single_real = this.checkDiapason(this.coords.p_single_real, this.options.from_min, this.options.from_max);
-                    this.coords.p_single = this.toFixed(this.coords.p_single_real / 100 * real_width);
+
+                    this.coords.p_single_fake = this.convertToFakePercent(this.coords.p_single_real);
 
                     break;
 
@@ -826,14 +999,16 @@
                         break;
                     }
 
-                    this.coords.p_from_real = this.calcWithStep(real_x / real_width * 100);
+                    this.coords.p_from_real = this.convertToRealPercent(handle_x);
+                    this.coords.p_from_real = this.calcWithStep(this.coords.p_from_real);
                     if (this.coords.p_from_real > this.coords.p_to_real) {
                         this.coords.p_from_real = this.coords.p_to_real;
                     }
                     this.coords.p_from_real = this.checkDiapason(this.coords.p_from_real, this.options.from_min, this.options.from_max);
                     this.coords.p_from_real = this.checkMinInterval(this.coords.p_from_real, this.coords.p_to_real, "from");
                     this.coords.p_from_real = this.checkMaxInterval(this.coords.p_from_real, this.coords.p_to_real, "from");
-                    this.coords.p_from = this.toFixed(this.coords.p_from_real / 100 * real_width);
+
+                    this.coords.p_from_fake = this.convertToFakePercent(this.coords.p_from_real);
 
                     break;
 
@@ -842,14 +1017,16 @@
                         break;
                     }
 
-                    this.coords.p_to_real = this.calcWithStep(real_x / real_width * 100);
+                    this.coords.p_to_real = this.convertToRealPercent(handle_x);
+                    this.coords.p_to_real = this.calcWithStep(this.coords.p_to_real);
                     if (this.coords.p_to_real < this.coords.p_from_real) {
                         this.coords.p_to_real = this.coords.p_from_real;
                     }
                     this.coords.p_to_real = this.checkDiapason(this.coords.p_to_real, this.options.to_min, this.options.to_max);
                     this.coords.p_to_real = this.checkMinInterval(this.coords.p_to_real, this.coords.p_from_real, "to");
                     this.coords.p_to_real = this.checkMaxInterval(this.coords.p_to_real, this.coords.p_from_real, "to");
-                    this.coords.p_to = this.toFixed(this.coords.p_to_real / 100 * real_width);
+
+                    this.coords.p_to_fake = this.convertToFakePercent(this.coords.p_to_real);
 
                     break;
 
@@ -858,38 +1035,75 @@
                         break;
                     }
 
-                    real_x = this.toFixed(real_x + (this.coords.p_handle * 0.1));
+                    handle_x = this.toFixed(handle_x + (this.coords.p_handle * 0.1));
 
-                    this.coords.p_from_real = this.calcWithStep((real_x - this.coords.p_gap_left) / real_width * 100);
+                    this.coords.p_from_real = this.convertToRealPercent(handle_x) - this.coords.p_gap_left;
+                    this.coords.p_from_real = this.calcWithStep(this.coords.p_from_real);
                     this.coords.p_from_real = this.checkDiapason(this.coords.p_from_real, this.options.from_min, this.options.from_max);
                     this.coords.p_from_real = this.checkMinInterval(this.coords.p_from_real, this.coords.p_to_real, "from");
-                    this.coords.p_from = this.toFixed(this.coords.p_from_real / 100 * real_width);
+                    this.coords.p_from_fake = this.convertToFakePercent(this.coords.p_from_real);
 
-                    this.coords.p_to_real = this.calcWithStep((real_x + this.coords.p_gap_right) / real_width * 100);
+                    this.coords.p_to_real = this.convertToRealPercent(handle_x) + this.coords.p_gap_right;
+                    this.coords.p_to_real = this.calcWithStep(this.coords.p_to_real);
                     this.coords.p_to_real = this.checkDiapason(this.coords.p_to_real, this.options.to_min, this.options.to_max);
                     this.coords.p_to_real = this.checkMinInterval(this.coords.p_to_real, this.coords.p_from_real, "to");
-                    this.coords.p_to = this.toFixed(this.coords.p_to_real / 100 * real_width);
+                    this.coords.p_to_fake = this.convertToFakePercent(this.coords.p_to_real);
+
+                    break;
+
+                case "both_one":
+                    if (this.options.from_fixed || this.options.to_fixed) {
+                        break;
+                    }
+
+                    var real_x = this.convertToRealPercent(handle_x),
+                        from = this.result.from_percent,
+                        to = this.result.to_percent,
+                        full = to - from,
+                        half = full / 2,
+                        new_from = real_x - half,
+                        new_to = real_x + half;
+
+                    if (new_from < 0) {
+                        new_from = 0;
+                        new_to = new_from + full;
+                    }
+
+                    if (new_to > 100) {
+                        new_to = 100;
+                        new_from = new_to - full;
+                    }
+
+                    this.coords.p_from_real = this.calcWithStep(new_from);
+                    this.coords.p_from_real = this.checkDiapason(this.coords.p_from_real, this.options.from_min, this.options.from_max);
+                    this.coords.p_from_fake = this.convertToFakePercent(this.coords.p_from_real);
+
+                    this.coords.p_to_real = this.calcWithStep(new_to);
+                    this.coords.p_to_real = this.checkDiapason(this.coords.p_to_real, this.options.to_min, this.options.to_max);
+                    this.coords.p_to_fake = this.convertToFakePercent(this.coords.p_to_real);
 
                     break;
             }
 
             if (this.options.type === "single") {
                 this.coords.p_bar_x = (this.coords.p_handle / 2);
-                this.coords.p_bar_w = this.coords.p_single;
+                this.coords.p_bar_w = this.coords.p_single_fake;
 
                 this.result.from_percent = this.coords.p_single_real;
-                this.result.from = this.calcReal(this.coords.p_single_real);
+                this.result.from = this.convertToValue(this.coords.p_single_real);
+
                 if (this.options.values.length) {
                     this.result.from_value = this.options.values[this.result.from];
                 }
             } else {
-                this.coords.p_bar_x = this.toFixed(this.coords.p_from + (this.coords.p_handle / 2));
-                this.coords.p_bar_w = this.toFixed(this.coords.p_to - this.coords.p_from);
+                this.coords.p_bar_x = this.toFixed(this.coords.p_from_fake + (this.coords.p_handle / 2));
+                this.coords.p_bar_w = this.toFixed(this.coords.p_to_fake - this.coords.p_from_fake);
 
                 this.result.from_percent = this.coords.p_from_real;
-                this.result.from = this.calcReal(this.coords.p_from_real);
+                this.result.from = this.convertToValue(this.coords.p_from_real);
                 this.result.to_percent = this.coords.p_to_real;
-                this.result.to = this.calcReal(this.coords.p_to_real);
+                this.result.to = this.convertToValue(this.coords.p_to_real);
+
                 if (this.options.values.length) {
                     this.result.from_value = this.options.values[this.result.from];
                     this.result.to_value = this.options.values[this.result.to];
@@ -900,7 +1114,11 @@
             this.calcLabels();
         },
 
-        calcPointer: function () {
+
+        /**
+         * calculates pointer X in percent
+         */
+        calcPointerPercent: function () {
             if (!this.coords.w_rs) {
                 this.coords.p_pointer = 0;
                 return;
@@ -915,6 +1133,45 @@
             this.coords.p_pointer = this.toFixed(this.coords.x_pointer / this.coords.w_rs * 100);
         },
 
+        convertToRealPercent: function (fake) {
+            var full = 100 - this.coords.p_handle;
+            return fake / full * 100;
+        },
+
+        convertToFakePercent: function (real) {
+            var full = 100 - this.coords.p_handle;
+            return real / 100 * full;
+        },
+
+        getHandleX: function () {
+            var max = 100 - this.coords.p_handle,
+                x = this.toFixed(this.coords.p_pointer - this.coords.p_gap);
+
+            if (x < 0) {
+                x = 0;
+            } else if (x > max) {
+                x = max;
+            }
+
+            return x;
+        },
+
+        calcHandlePercent: function () {
+            if (this.options.type === "single") {
+                this.coords.w_handle = this.$cache.s_single.outerWidth(false);
+            } else {
+                this.coords.w_handle = this.$cache.s_from.outerWidth(false);
+            }
+
+            this.coords.p_handle = this.toFixed(this.coords.w_handle / this.coords.w_rs * 100);
+        },
+
+        /**
+         * Find closest handle to pointer click
+         *
+         * @param real_x {Number}
+         * @returns {String}
+         */
         chooseHandle: function (real_x) {
             if (this.options.type === "single") {
                 return "single";
@@ -928,6 +1185,9 @@
             }
         },
 
+        /**
+         * Measure Min and Max labels width in percent
+         */
         calcMinMax: function () {
             if (!this.coords.w_rs) {
                 return;
@@ -937,6 +1197,9 @@
             this.labels.p_max = this.labels.w_max / this.coords.w_rs * 100;
         },
 
+        /**
+         * Measure labels width and X in percent
+         */
         calcLabels: function () {
             if (!this.coords.w_rs || this.options.hide_from_to) {
                 return;
@@ -945,29 +1208,29 @@
             if (this.options.type === "single") {
 
                 this.labels.w_single = this.$cache.single.outerWidth(false);
-                this.labels.p_single = this.labels.w_single / this.coords.w_rs * 100;
-                this.labels.p_single_left = this.coords.p_single + (this.coords.p_handle / 2) - (this.labels.p_single / 2);
-                this.labels.p_single_left = this.checkEdges(this.labels.p_single_left, this.labels.p_single);
+                this.labels.p_single_fake = this.labels.w_single / this.coords.w_rs * 100;
+                this.labels.p_single_left = this.coords.p_single_fake + (this.coords.p_handle / 2) - (this.labels.p_single_fake / 2);
+                this.labels.p_single_left = this.checkEdges(this.labels.p_single_left, this.labels.p_single_fake);
 
             } else {
 
                 this.labels.w_from = this.$cache.from.outerWidth(false);
-                this.labels.p_from = this.labels.w_from / this.coords.w_rs * 100;
-                this.labels.p_from_left = this.coords.p_from + (this.coords.p_handle / 2) - (this.labels.p_from / 2);
+                this.labels.p_from_fake = this.labels.w_from / this.coords.w_rs * 100;
+                this.labels.p_from_left = this.coords.p_from_fake + (this.coords.p_handle / 2) - (this.labels.p_from_fake / 2);
                 this.labels.p_from_left = this.toFixed(this.labels.p_from_left);
-                this.labels.p_from_left = this.checkEdges(this.labels.p_from_left, this.labels.p_from);
+                this.labels.p_from_left = this.checkEdges(this.labels.p_from_left, this.labels.p_from_fake);
 
                 this.labels.w_to = this.$cache.to.outerWidth(false);
-                this.labels.p_to = this.labels.w_to / this.coords.w_rs * 100;
-                this.labels.p_to_left = this.coords.p_to + (this.coords.p_handle / 2) - (this.labels.p_to / 2);
+                this.labels.p_to_fake = this.labels.w_to / this.coords.w_rs * 100;
+                this.labels.p_to_left = this.coords.p_to_fake + (this.coords.p_handle / 2) - (this.labels.p_to_fake / 2);
                 this.labels.p_to_left = this.toFixed(this.labels.p_to_left);
-                this.labels.p_to_left = this.checkEdges(this.labels.p_to_left, this.labels.p_to);
+                this.labels.p_to_left = this.checkEdges(this.labels.p_to_left, this.labels.p_to_fake);
 
                 this.labels.w_single = this.$cache.single.outerWidth(false);
-                this.labels.p_single = this.labels.w_single / this.coords.w_rs * 100;
-                this.labels.p_single_left = ((this.labels.p_from_left + this.labels.p_to_left + this.labels.p_to) / 2) - (this.labels.p_single / 2);
+                this.labels.p_single_fake = this.labels.w_single / this.coords.w_rs * 100;
+                this.labels.p_single_left = ((this.labels.p_from_left + this.labels.p_to_left + this.labels.p_to_fake) / 2) - (this.labels.p_single_fake / 2);
                 this.labels.p_single_left = this.toFixed(this.labels.p_single_left);
-                this.labels.p_single_left = this.checkEdges(this.labels.p_single_left, this.labels.p_single);
+                this.labels.p_single_left = this.checkEdges(this.labels.p_single_left, this.labels.p_single_fake);
 
             }
         },
@@ -977,6 +1240,10 @@
         // =============================================================================================================
         // Drawings
 
+        /**
+         * Main function called in request animation frame
+         * to update everything
+         */
         updateScene: function () {
             if (this.raf_id) {
                 cancelAnimationFrame(this.raf_id);
@@ -999,6 +1266,9 @@
             }
         },
 
+        /**
+         * Draw handles
+         */
         drawHandles: function () {
             this.coords.w_rs = this.$cache.rs.outerWidth(false);
 
@@ -1040,20 +1310,19 @@
                 this.$cache.bar[0].style.width = this.coords.p_bar_w + "%";
 
                 if (this.options.type === "single") {
-                    this.$cache.s_single[0].style.left = this.coords.p_single + "%";
+                    this.$cache.s_single[0].style.left = this.coords.p_single_fake + "%";
 
                     this.$cache.single[0].style.left = this.labels.p_single_left + "%";
 
                     if (this.options.values.length) {
                         this.$cache.input.prop("value", this.result.from_value);
-                        this.$cache.input.data("from", this.result.from_value);
                     } else {
                         this.$cache.input.prop("value", this.result.from);
-                        this.$cache.input.data("from", this.result.from);
                     }
+                    this.$cache.input.data("from", this.result.from);
                 } else {
-                    this.$cache.s_from[0].style.left = this.coords.p_from + "%";
-                    this.$cache.s_to[0].style.left = this.coords.p_to + "%";
+                    this.$cache.s_from[0].style.left = this.coords.p_from_fake + "%";
+                    this.$cache.s_to[0].style.left = this.coords.p_to_fake + "%";
 
                     if (this.old_from !== this.result.from || this.force_redraw) {
                         this.$cache.from[0].style.left = this.labels.p_from_left + "%";
@@ -1065,14 +1334,12 @@
                     this.$cache.single[0].style.left = this.labels.p_single_left + "%";
 
                     if (this.options.values.length) {
-                        this.$cache.input.prop("value", this.result.from_value + ";" + this.result.to_value);
-                        this.$cache.input.data("from", this.result.from_value);
-                        this.$cache.input.data("to", this.result.to_value);
+                        this.$cache.input.prop("value", this.result.from_value + this.options.input_values_separator + this.result.to_value);
                     } else {
-                        this.$cache.input.prop("value", this.result.from + ";" + this.result.to);
-                        this.$cache.input.data("from", this.result.from);
-                        this.$cache.input.data("to", this.result.to);
+                        this.$cache.input.prop("value", this.result.from + this.options.input_values_separator + this.result.to);
                     }
+                    this.$cache.input.data("from", this.result.from);
+                    this.$cache.input.data("to", this.result.to);
                 }
 
                 if ((this.old_from !== this.result.from || this.old_to !== this.result.to) && !this.is_start) {
@@ -1082,18 +1349,19 @@
                 this.old_from = this.result.from;
                 this.old_to = this.result.to;
 
-                var is_function = this.options.onChange && typeof this.options.onChange === "function" && !this.is_resize;
-                if (is_function && !this.is_update && !this.is_start) {
-                    this.options.onChange(this.result);
+                // callbacks call
+                if (!this.is_resize && !this.is_update && !this.is_start && !this.is_finish) {
+                    this.callOnChange();
                 }
-
-                var is_finish = this.options.onFinish && typeof this.options.onFinish === "function";
-                if (is_finish && (this.is_key || this.is_click)) {
-                    this.options.onFinish(this.result);
+                if (this.is_key || this.is_click) {
+                    this.is_key = false;
+                    this.is_click = false;
+                    this.callOnFinish();
                 }
 
                 this.is_update = false;
                 this.is_resize = false;
+                this.is_finish = false;
             }
 
             this.is_start = false;
@@ -1102,6 +1370,11 @@
             this.force_redraw = false;
         },
 
+        /**
+         * Draw labels
+         * measure labels collisions
+         * collapse close labels
+         */
         drawLabels: function () {
             if (!this.options) {
                 return;
@@ -1135,7 +1408,7 @@
                     this.$cache.min[0].style.visibility = "visible";
                 }
 
-                if (this.labels.p_single_left + this.labels.p_single > 100 - this.labels.p_max - 1) {
+                if (this.labels.p_single_left + this.labels.p_single_fake > 100 - this.labels.p_max - 1) {
                     this.$cache.max[0].style.visibility = "hidden";
                 } else {
                     this.$cache.max[0].style.visibility = "visible";
@@ -1180,21 +1453,26 @@
                 this.calcLabels();
 
                 var min = Math.min(this.labels.p_single_left, this.labels.p_from_left),
-                    single_left = this.labels.p_single_left + this.labels.p_single,
-                    to_left = this.labels.p_to_left + this.labels.p_to,
+                    single_left = this.labels.p_single_left + this.labels.p_single_fake,
+                    to_left = this.labels.p_to_left + this.labels.p_to_fake,
                     max = Math.max(single_left, to_left);
 
-                if (this.labels.p_from_left + this.labels.p_from >= this.labels.p_to_left) {
+                if (this.labels.p_from_left + this.labels.p_from_fake >= this.labels.p_to_left) {
                     this.$cache.from[0].style.visibility = "hidden";
                     this.$cache.to[0].style.visibility = "hidden";
                     this.$cache.single[0].style.visibility = "visible";
 
                     if (this.result.from === this.result.to) {
-                        this.$cache.from[0].style.visibility = "visible";
+                        if (this.target === "from") {
+                            this.$cache.from[0].style.visibility = "visible";
+                        } else if (this.target === "to") {
+                            this.$cache.to[0].style.visibility = "visible";
+                        }
                         this.$cache.single[0].style.visibility = "hidden";
                         max = to_left;
                     } else {
                         this.$cache.from[0].style.visibility = "hidden";
+                        this.$cache.to[0].style.visibility = "hidden";
                         this.$cache.single[0].style.visibility = "visible";
                         max = Math.max(single_left, to_left);
                     }
@@ -1219,6 +1497,9 @@
             }
         },
 
+        /**
+         * Draw shadow intervals
+         */
         drawShadow: function () {
             var o = this.options,
                 c = this.$cache,
@@ -1235,8 +1516,8 @@
 
             if (o.type === "single") {
                 if (o.from_shadow && (is_from_min || is_from_max)) {
-                    from_min = this.calcPercent(is_from_min ? o.from_min : o.min);
-                    from_max = this.calcPercent(is_from_max ? o.from_max : o.max) - from_min;
+                    from_min = this.convertToPercent(is_from_min ? o.from_min : o.min);
+                    from_max = this.convertToPercent(is_from_max ? o.from_max : o.max) - from_min;
                     from_min = this.toFixed(from_min - (this.coords.p_handle / 100 * from_min));
                     from_max = this.toFixed(from_max - (this.coords.p_handle / 100 * from_max));
                     from_min = from_min + (this.coords.p_handle / 2);
@@ -1249,8 +1530,8 @@
                 }
             } else {
                 if (o.from_shadow && (is_from_min || is_from_max)) {
-                    from_min = this.calcPercent(is_from_min ? o.from_min : o.min);
-                    from_max = this.calcPercent(is_from_max ? o.from_max : o.max) - from_min;
+                    from_min = this.convertToPercent(is_from_min ? o.from_min : o.min);
+                    from_max = this.convertToPercent(is_from_max ? o.from_max : o.max) - from_min;
                     from_min = this.toFixed(from_min - (this.coords.p_handle / 100 * from_min));
                     from_max = this.toFixed(from_max - (this.coords.p_handle / 100 * from_max));
                     from_min = from_min + (this.coords.p_handle / 2);
@@ -1263,8 +1544,8 @@
                 }
 
                 if (o.to_shadow && (is_to_min || is_to_max)) {
-                    to_min = this.calcPercent(is_to_min ? o.to_min : o.min);
-                    to_max = this.calcPercent(is_to_max ? o.to_max : o.max) - to_min;
+                    to_min = this.convertToPercent(is_to_min ? o.to_min : o.min);
+                    to_max = this.convertToPercent(is_to_max ? o.to_max : o.max) - to_min;
                     to_min = this.toFixed(to_min - (this.coords.p_handle / 100 * to_min));
                     to_max = this.toFixed(to_max - (this.coords.p_handle / 100 * to_max));
                     to_min = to_min + (this.coords.p_handle / 2);
@@ -1281,20 +1562,73 @@
 
 
         // =============================================================================================================
+        // Callbacks
+
+        callOnStart: function () {
+            if (this.options.onStart && typeof this.options.onStart === "function") {
+                this.options.onStart(this.result);
+            }
+        },
+        callOnChange: function () {
+            if (this.options.onChange && typeof this.options.onChange === "function") {
+                this.options.onChange(this.result);
+            }
+        },
+        callOnFinish: function () {
+            if (this.options.onFinish && typeof this.options.onFinish === "function") {
+                this.options.onFinish(this.result);
+            }
+        },
+        callOnUpdate: function () {
+            if (this.options.onUpdate && typeof this.options.onUpdate === "function") {
+                this.options.onUpdate(this.result);
+            }
+        },
+
+
+
+        // =============================================================================================================
         // Service methods
 
         toggleInput: function () {
             this.$cache.input.toggleClass("irs-hidden-input");
         },
 
-        calcPercent: function (num) {
-            var w = (this.options.max - this.options.min) / 100,
-                percent = (num - this.options.min) / w;
+        /**
+         * Convert real value to percent
+         *
+         * @param value {Number} X in real
+         * @param no_min {boolean=} don't use min value
+         * @returns {Number} X in percent
+         */
+        convertToPercent: function (value, no_min) {
+            var diapason = this.options.max - this.options.min,
+                one_percent = diapason / 100,
+                val, percent;
+
+            if (!diapason) {
+                this.no_diapason = true;
+                return 0;
+            }
+
+            if (no_min) {
+                val = value;
+            } else {
+                val = value - this.options.min;
+            }
+
+            percent = val / one_percent;
 
             return this.toFixed(percent);
         },
 
-        calcReal: function (percent) {
+        /**
+         * Convert percent to real values
+         *
+         * @param percent {Number} X in percent
+         * @returns {Number} X in real
+         */
+        convertToValue: function (percent) {
             var min = this.options.min,
                 max = this.options.max,
                 min_decimals = min.toString().split(".")[1],
@@ -1361,6 +1695,12 @@
             return result;
         },
 
+        /**
+         * Round percent value with step
+         *
+         * @param percent {Number}
+         * @returns percent {Number} rounded
+         */
         calcWithStep: function (percent) {
             var rounded = Math.round(percent / this.coords.p_step) * this.coords.p_step;
 
@@ -1383,8 +1723,8 @@
                 return p_current;
             }
 
-            current = this.calcReal(p_current);
-            next = this.calcReal(p_next);
+            current = this.convertToValue(p_current);
+            next = this.convertToValue(p_next);
 
             if (type === "from") {
 
@@ -1400,7 +1740,7 @@
 
             }
 
-            return this.calcPercent(current);
+            return this.convertToPercent(current);
         },
 
         checkMaxInterval: function (p_current, p_next, type) {
@@ -1412,8 +1752,8 @@
                 return p_current;
             }
 
-            current = this.calcReal(p_current);
-            next = this.calcReal(p_next);
+            current = this.convertToValue(p_current);
+            next = this.convertToValue(p_next);
 
             if (type === "from") {
 
@@ -1429,11 +1769,11 @@
 
             }
 
-            return this.calcPercent(current);
+            return this.convertToPercent(current);
         },
 
         checkDiapason: function (p_num, min, max) {
-            var num = this.calcReal(p_num),
+            var num = this.convertToValue(p_num),
                 o = this.options;
 
             if (typeof min !== "number") {
@@ -1452,7 +1792,7 @@
                 num = max;
             }
 
-            return this.calcPercent(num);
+            return this.convertToPercent(num);
         },
 
         toFixed: function (num) {
@@ -1513,13 +1853,8 @@
             if (typeof o.keyboard_step === "string") o.keyboard_step = +o.keyboard_step;
             if (typeof o.grid_num === "string") o.grid_num = +o.grid_num;
 
-            if (o.max <= o.min) {
-                if (o.min) {
-                    o.max = o.min * 2;
-                } else {
-                    o.max = o.min + 1;
-                }
-                o.step = 1;
+            if (o.max < o.min) {
+                o.max = o.min;
             }
 
             if (vl) {
@@ -1669,7 +2004,7 @@
 
         updateFrom: function () {
             this.result.from = this.options.from;
-            this.result.from_percent = this.calcPercent(this.result.from);
+            this.result.from_percent = this.convertToPercent(this.result.from);
             if (this.options.values) {
                 this.result.from_value = this.options.values[this.result.from];
             }
@@ -1677,7 +2012,7 @@
 
         updateTo: function () {
             this.result.to = this.options.to;
-            this.result.to_percent = this.calcPercent(this.result.to);
+            this.result.to_percent = this.convertToPercent(this.result.to);
             if (this.options.values) {
                 this.result.to_value = this.options.values[this.result.to];
             }
@@ -1768,7 +2103,7 @@
 
                 html += '<span class="irs-grid-pol" style="left: ' + big_w + '%"></span>';
 
-                result = this.calcReal(big_w);
+                result = this.convertToValue(big_w);
                 if (o.values.length) {
                     result = o.p_values[result];
                 } else {
diff --git a/inst/www/shared/ionrangeslider/js/ion.rangeSlider.min.js b/inst/www/shared/ionrangeslider/js/ion.rangeSlider.min.js
index d5fcfbc..f70c628 100644
--- a/inst/www/shared/ionrangeslider/js/ion.rangeSlider.min.js
+++ b/inst/www/shared/ionrangeslider/js/ion.rangeSlider.min.js
@@ -1,2 +1,2 @@
-!function(a,b,c,d){"use strict";var e=0,f=function(){var b,c=d.userAgent,e=/msie\s\d+/i;return c.search(e)>0&&(b=e.exec(c).toString(),b=b.split(" ")[1],9>b)?(a("html").addClass("lt-ie9"),!0):!1}();Function.prototype.bind||(Function.prototype.bind=function(a){var b=this,c=[].slice;if("function"!=typeof b)throw new TypeError;var d=c.call(arguments,1),e=function(){if(this instanceof e){var f=function(){};f.prototype=b.prototype;var g=new f,h=b.apply(g,d.concat(c.call(arguments)));return Obj [...]
-h>7&&(k=2),h>14&&(k=1),h>28&&(k=0),a=0;h+1>a;a++){for(c=k,j=this.toFixed(i*a),j>100&&(j=100,c-=2,0>c&&(c=0)),this.coords.big[a]=j,d=(j-i*(a-1))/(c+1),b=1;c>=b&&0!==j;b++)l=this.toFixed(j-d*b),m+='<span class="irs-grid-pol small" style="left: '+l+'%"></span>';m+='<span class="irs-grid-pol" style="left: '+j+'%"></span>',e=this.calcReal(j),e=f.values.length?f.p_values[e]:this._prettify(e),m+='<span class="irs-grid-text js-grid-text-'+a+'" style="left: '+j+'%">'+e+"</span>"}this.coords.big_n [...]
\ No newline at end of file
+!function(a,b,c,d,e){"use strict";var f=0,g=function(){var b,c=d.userAgent,e=/msie\s\d+/i;return c.search(e)>0&&(b=e.exec(c).toString(),b=b.split(" ")[1],b<9)&&(a("html").addClass("lt-ie9"),!0)}();Function.prototype.bind||(Function.prototype.bind=function(a){var b=this,c=[].slice;if("function"!=typeof b)throw new TypeError;var d=c.call(arguments,1),e=function(){if(this instanceof e){var f=function(){};f.prototype=b.prototype;var g=new f,h=b.apply(g,d.concat(c.call(arguments)));return Obj [...]
+d<b&&(d=b),d>c&&(d=c),this.convertToPercent(d)},toFixed:function(a){return a=a.toFixed(9),+a},_prettify:function(a){return this.options.prettify_enabled?this.options.prettify&&"function"==typeof this.options.prettify?this.options.prettify(a):this.prettify(a):a},prettify:function(a){var b=a.toString();return b.replace(/(\d{1,3}(?=(?:\d\d\d)+(?!\d)))/g,"$1"+this.options.prettify_separator)},checkEdges:function(a,b){return this.options.force_edges?(a<0?a=0:a>100-b&&(a=100-b),this.toFixed(a) [...]
\ No newline at end of file
diff --git a/inst/www/shared/jquery-AUTHORS.txt b/inst/www/shared/jquery-AUTHORS.txt
index 14a0e68..16bc6c9 100644
--- a/inst/www/shared/jquery-AUTHORS.txt
+++ b/inst/www/shared/jquery-AUTHORS.txt
@@ -36,7 +36,7 @@ Justin Meyer <justinbmeyer at gmail.com>
 Ben Alman <cowboy at rj3.net>
 James Padolsey <cla at padolsey.net>
 David Petersen <public at petersendidit.com>
-Batiste Bieler <batiste at gmail.com>
+Batiste Bieler <batiste.bieler at gmail.com>
 Alexander Farkas <info at corrupt-system.de>
 Rick Waldron <waldron.rick at gmail.com>
 Filipe Fortes <filipe at fortes.com>
@@ -215,3 +215,52 @@ John Hoven <hovenj at gmail.com>
 Christian Kosmowski <ksmwsk at gmail.com>
 Liang Peng <poppinlp at gmail.com>
 TJ VanToll <tj.vantoll at gmail.com>
+Senya Pugach <upisfree at outlook.com>
+Aurelio De Rosa <aurelioderosa at gmail.com>
+Nazar Mokrynskyi <nazar at mokrynskyi.com>
+Arthur Verschaeve <contact at arthurverschaeve.be>
+Dan Hart <danhart at notonthehighstreet.com>
+Scott González <scott.gonzalez at gmail.com>
+Zheming Sun <mescodasun at gmail.com>
+Bin Xin <rhyzix at gmail.com>
+David Corbacho <davidcorbacho at gmail.com>
+Veaceslav Grimalschi <grimalschi at yandex.ru>
+Daniel Husar <dano.husar at gmail.com>
+Jason Bedard <jason+github at jbedard.ca>
+Ben Toews <mastahyeti at gmail.com>
+Aditya Raghavan <araghavan3 at gmail.com>
+Nicolas HENRY <icewil at gmail.com>
+Norman Xu <homyu.shinn at gmail.com>
+Anne-Gaelle Colom <coloma at westminster.ac.uk>
+Victor Homyakov <vkhomyackov at gmail.com>
+George Mauer <gmauer at gmail.com>
+Leonardo Braga <leonardo.braga at gmail.com>
+Stephen Edgar <stephen at netweb.com.au>
+Thomas Tortorini <thomastortorini at gmail.com>
+Winston Howes <winstonhowes at gmail.com>
+Jon Hester <jon.d.hester at gmail.com>
+Alexander O'Mara <me at alexomara.com>
+Bastian Buchholz <buchholz.bastian at googlemail.com>
+Arthur Stolyar <nekr.fabula at gmail.com>
+Calvin Metcalf <calvin.metcalf at gmail.com>
+Mu Haibao <mhbseal at 163.com>
+Richard McDaniel <rm0026 at uah.edu>
+Chris Rebert <github at rebertia.com>
+Gilad Peleg <giladp007 at gmail.com>
+Martin Naumann <martin at geekonaut.de>
+Bruno Pérel <brunoperel at gmail.com>
+Reed Loden <reed at reedloden.com>
+Daniel Nill <daniellnill at gmail.com>
+Yongwoo Jeon <yongwoo.jeon at navercorp.com>
+Sean Henderson <seanh.za at gmail.com>
+Adrian Olek <adrianolek at gmail.com>
+Richard Kraaijenhagen <stdin+git at riichard.com>
+Gary Ye <garysye at gmail.com>
+Christian Grete <webmaster at christiangrete.com>
+Liza Ramo <liza.h.ramo at gmail.com>
+Joelle Fleurantin <joasqueeniebee at gmail.com>
+Julian Alexander Murillo <julian.alexander.murillo at gmail.com>
+Jun Sun <klsforever at gmail.com>
+Devin Wilson <dwilson6.github at gmail.com>
+Todor Prikumov <tono_pr at abv.bg>
+Zack Hall <zackhall at outlook.com>
diff --git a/inst/www/shared/jqueryui/AUTHORS.txt b/inst/www/shared/jqueryui/AUTHORS.txt
new file mode 100644
index 0000000..a75056b
--- /dev/null
+++ b/inst/www/shared/jqueryui/AUTHORS.txt
@@ -0,0 +1,333 @@
+Authors ordered by first contribution
+A list of current team members is available at http://jqueryui.com/about
+
+Paul Bakaus <paul.bakaus at gmail.com>
+Richard Worth <rdworth at gmail.com>
+Yehuda Katz <wycats at gmail.com>
+Sean Catchpole <sean at sunsean.com>
+John Resig <jeresig at gmail.com>
+Tane Piper <piper.tane at gmail.com>
+Dmitri Gaskin <dmitrig01 at gmail.com>
+Klaus Hartl <klaus.hartl at gmail.com>
+Stefan Petre <stefan.petre at gmail.com>
+Gilles van den Hoven <gilles at webunity.nl>
+Micheil Bryan Smith <micheil at brandedcode.com>
+Jörn Zaefferer <joern.zaefferer at gmail.com>
+Marc Grabanski <m at marcgrabanski.com>
+Keith Wood <kbwood at iinet.com.au>
+Brandon Aaron <brandon.aaron at gmail.com>
+Scott González <scott.gonzalez at gmail.com>
+Eduardo Lundgren <eduardolundgren at gmail.com>
+Aaron Eisenberger <aaronchi at gmail.com>
+Joan Piedra <theneojp at gmail.com>
+Bruno Basto <b.basto at gmail.com>
+Remy Sharp <remy at leftlogic.com>
+Bohdan Ganicky <bohdan.ganicky at gmail.com>
+David Bolter <david.bolter at gmail.com>
+Chi Cheng <cloudream at gmail.com>
+Ca-Phun Ung <pazu2k at gmail.com>
+Ariel Flesler <aflesler at gmail.com>
+Maggie Wachs <maggie at filamentgroup.com>
+Scott Jehl <scottjehl at gmail.com>
+Todd Parker <todd at filamentgroup.com>
+Andrew Powell <andrew at shellscape.org>
+Brant Burnett <btburnett3 at gmail.com>
+Douglas Neiner <doug at dougneiner.com>
+Paul Irish <paul.irish at gmail.com>
+Ralph Whitbeck <ralph.whitbeck at gmail.com>
+Thibault Duplessis <thibault.duplessis at gmail.com>
+Dominique Vincent <dominique.vincent at toitl.com>
+Jack Hsu <jack.hsu at gmail.com>
+Adam Sontag <ajpiano at ajpiano.com>
+Carl Fürstenberg <carl at excito.com>
+Kevin Dalman <development at allpro.net>
+Alberto Fernández Capel <afcapel at gmail.com>
+Jacek Jędrzejewski (http://jacek.jedrzejewski.name)
+Ting Kuei <ting at kuei.com>
+Samuel Cormier-Iijima <sam at chide.it>
+Jon Palmer <jonspalmer at gmail.com>
+Ben Hollis <bhollis at amazon.com>
+Justin MacCarthy <Justin at Rubystars.biz>
+Eyal Kobrigo <kobrigo at hotmail.com>
+Tiago Freire <tiago.freire at gmail.com>
+Diego Tres <diegotres at gmail.com>
+Holger Rüprich <holger at rueprich.de>
+Ziling Zhao <zilingzhao at gmail.com>
+Mike Alsup <malsup at gmail.com>
+Robson Braga Araujo <robsonbraga at gmail.com>
+Pierre-Henri Ausseil <ph.ausseil at gmail.com>
+Christopher McCulloh <cmcculloh at gmail.com>
+Andrew Newcomb <ext.github at preceptsoftware.co.uk>
+Lim Chee Aun <cheeaun at gmail.com>
+Jorge Barreiro <yortx.barry at gmail.com>
+Daniel Steigerwald <daniel at steigerwald.cz>
+John Firebaugh <john_firebaugh at bigfix.com>
+John Enters <github at darkdark.net>
+Andrey Kapitcyn <ru.m157y at gmail.com>
+Dmitry Petrov <dpetroff at gmail.com>
+Eric Hynds <eric at hynds.net>
+Chairat Sunthornwiphat <pipo at sixhead.com>
+Josh Varner <josh.varner at gmail.com>
+Stéphane Raimbault <stephane.raimbault at gmail.com>
+Jay Merrifield <fracmak at gmail.com>
+J. Ryan Stinnett <jryans at gmail.com>
+Peter Heiberg <peter at heiberg.se>
+Alex Dovenmuehle <adovenmuehle at gmail.com>
+Jamie Gegerson <git at jamiegegerson.com>
+Raymond Schwartz <skeetergraphics at gmail.com>
+Phillip Barnes <philbar at gmail.com>
+Kyle Wilkinson <kai at wikyd.org>
+Khaled AlHourani <me at khaledalhourani.com>
+Marian Rudzynski <mr at impaled.org>
+Jean-Francois Remy <jeff at melix.org>
+Doug Blood <dougblood at gmail.com>
+Filippo Cavallarin <filippo.cavallarin at codseq.it>
+Heiko Henning <heiko at thehennings.ch>
+Aliaksandr Rahalevich <saksmlz at gmail.com>
+Mario Visic <mario at mariovisic.com>
+Xavi Ramirez <xavi.rmz at gmail.com>
+Max Schnur <max.schnur at gmail.com>
+Saji Nediyanchath <saji89 at gmail.com>
+Corey Frang <gnarf37 at gmail.com>
+Aaron Peterson <aaronp123 at yahoo.com>
+Ivan Peters <ivan at ivanpeters.com>
+Mohamed Cherif Bouchelaghem <cherifbouchelaghem at yahoo.fr>
+Marcos Sousa <falecomigo at marcossousa.com>
+Michael DellaNoce <mdellanoce at mailtrust.com>
+George Marshall <echosx at gmail.com>
+Tobias Brunner <tobias at strongswan.org>
+Martin Solli <msolli at gmail.com>
+David Petersen <public at petersendidit.com>
+Dan Heberden <danheberden at gmail.com>
+William Kevin Manire <williamkmanire at gmail.com>
+Gilmore Davidson <gilmoreorless at gmail.com>
+Michael Wu <michaelmwu at gmail.com>
+Adam Parod <mystic414 at gmail.com>
+Guillaume Gautreau <guillaume+github at ghusse.com>
+Marcel Toele <EleotleCram at gmail.com>
+Dan Streetman <ddstreet at ieee.org>
+Matt Hoskins <matt at nipltd.com>
+Giovanni Giacobbi <giovanni at giacobbi.net>
+Kyle Florence <kyle.florence at gmail.com>
+Pavol Hluchý <lopo at losys.sk>
+Hans Hillen <hans.hillen at gmail.com>
+Mark Johnson <virgofx at live.com>
+Trey Hunner <treyhunner at gmail.com>
+Shane Whittet <whittet at gmail.com>
+Edward A Faulkner <ef at alum.mit.edu>
+Adam Baratz <adam at adambaratz.com>
+Kato Kazuyoshi <kato.kazuyoshi at gmail.com>
+Eike Send <eike.send at gmail.com>
+Kris Borchers <kris.borchers at gmail.com>
+Eddie Monge <eddie at eddiemonge.com>
+Israel Tsadok <itsadok at gmail.com>
+Carson McDonald <carson at ioncannon.net>
+Jason Davies <jason at jasondavies.com>
+Garrison Locke <gplocke at gmail.com>
+David Murdoch <david at davidmurdoch.com>
+Benjamin Scott Boyle <benjamins.boyle at gmail.com>
+Jesse Baird <jebaird at gmail.com>
+Jonathan Vingiano <jvingiano at gmail.com>
+Dylan Just <dev at ephox.com>
+Hiroshi Tomita <tomykaira at gmail.com>
+Glenn Goodrich <glenn.goodrich at gmail.com>
+Tarafder Ashek-E-Elahi <mail.ashek at gmail.com>
+Ryan Neufeld <ryan at neufeldmail.com>
+Marc Neuwirth <marc.neuwirth at gmail.com>
+Philip Graham <philip.robert.graham at gmail.com>
+Benjamin Sterling <benjamin.sterling at kenzomedia.com>
+Wesley Walser <waw325 at gmail.com>
+Kouhei Sutou <kou at clear-code.com>
+Karl Kirch <karlkrch at gmail.com>
+Chris Kelly <ckdake at ckdake.com>
+Jason Oster <jay at kodewerx.org>
+Felix Nagel <info at felixnagel.com>
+Alexander Polomoshnov <alex.polomoshnov at gmail.com>
+David Leal <dgleal at gmail.com>
+Igor Milla <igor.fsp.milla at gmail.com>
+Dave Methvin <dave.methvin at gmail.com>
+Florian Gutmann <f.gutmann at chronimo.com>
+Marwan Al Jubeh <marwan.aljubeh at gmail.com>
+Milan Broum <midlis at googlemail.com>
+Sebastian Sauer <info at dynpages.de>
+Gaëtan Muller <m.gaetan89 at gmail.com>
+Michel Weimerskirch <michel at weimerskirch.net>
+William Griffiths <william at ycymro.com>
+Stojce Slavkovski <stojce at gmail.com>
+David Soms <david.soms at gmail.com>
+David De Sloovere <david.desloovere at outlook.com>
+Michael P. Jung <michael.jung at terreon.de>
+Shannon Pekary <spekary at gmail.com>
+Dan Wellman <danwellman at hotmail.com>
+Matthew Edward Hutton <meh at corefiling.co.uk>
+James Khoury <james at jameskhoury.com>
+Rob Loach <robloach at gmail.com>
+Alberto Monteiro <betimbrasil at gmail.com>
+Alex Rhea <alex.rhea at gmail.com>
+Krzysztof Rosiński <rozwell69 at gmail.com>
+Ryan Olton <oltonr at gmail.com>
+Genie <386 at mail.com>
+Rick Waldron <waldron.rick at gmail.com>
+Ian Simpson <spoonlikesham at gmail.com>
+Lev Kitsis <spam4lev at gmail.com>
+TJ VanToll <tj.vantoll at gmail.com>
+Justin Domnitz <jdomnitz at gmail.com>
+Douglas Cerna <douglascerna at yahoo.com>
+Bert ter Heide <bertjh at hotmail.com>
+Jasvir Nagra <jasvir at gmail.com>
+Yuriy Khabarov <13real008 at gmail.com>
+Harri Kilpiö <harri.kilpio at gmail.com>
+Lado Lomidze <lado.lomidze at gmail.com>
+Amir E. Aharoni <amir.aharoni at mail.huji.ac.il>
+Simon Sattes <simon.sattes at gmail.com>
+Jo Liss <joliss42 at gmail.com>
+Guntupalli Karunakar <karunakarg at yahoo.com>
+Shahyar Ghobadpour <shahyar at gmail.com>
+Lukasz Lipinski <uzza17 at gmail.com>
+Timo Tijhof <krinklemail at gmail.com>
+Jason Moon <jmoon at socialcast.com>
+Martin Frost <martinf55 at hotmail.com>
+Eneko Illarramendi <eneko at illarra.com>
+EungJun Yi <semtlenori at gmail.com>
+Courtland Allen <courtlandallen at gmail.com>
+Viktar Varvanovich <non4eg at gmail.com>
+Danny Trunk <dtrunk90 at gmail.com>
+Pavel Stetina <pavel.stetina at nangu.tv>
+Michael Stay <metaweta at gmail.com>
+Steven Roussey <sroussey at gmail.com>
+Michael Hollis <hollis21 at gmail.com>
+Lee Rowlands <lee.rowlands at previousnext.com.au>
+Timmy Willison <timmywillisn at gmail.com>
+Karl Swedberg <kswedberg at gmail.com>
+Baoju Yuan <the_guy_1987 at hotmail.com>
+Maciej Mroziński <maciej.k.mrozinski at gmail.com>
+Luis Dalmolin <luis.nh at gmail.com>
+Mark Aaron Shirley <maspwr at gmail.com>
+Martin Hoch <martin at fidion.de>
+Jiayi Yang <tr870829 at gmail.com>
+Philipp Benjamin Köppchen <xgxtpbk at gws.ms>
+Sindre Sorhus <sindresorhus at gmail.com>
+Bernhard Sirlinger <bernhard.sirlinger at tele2.de>
+Jared A. Scheel <jared at jaredscheel.com>
+Rafael Xavier de Souza <rxaviers at gmail.com>
+John Chen <zhang.z.chen at intel.com>
+Robert Beuligmann <robertbeuligmann at gmail.com>
+Dale Kocian <dale.kocian at gmail.com>
+Mike Sherov <mike.sherov at gmail.com>
+Andrew Couch <andy at couchand.com>
+Marc-Andre Lafortune <github at marc-andre.ca>
+Nate Eagle <nate.eagle at teamaol.com>
+David Souther <davidsouther at gmail.com>
+Mathias Stenbom <mathias at stenbom.com>
+Sergey Kartashov <ebishkek at yandex.ru>
+Avinash R <nashpapa at gmail.com>
+Ethan Romba <ethanromba at gmail.com>
+Cory Gackenheimer <cory.gack at gmail.com>
+Juan Pablo Kaniefsky <jpkaniefsky at gmail.com>
+Roman Salnikov <bardt.dz at gmail.com>
+Anika Henke <anika at selfthinker.org>
+Samuel Bovée <samycookie2000 at yahoo.fr>
+Fabrício Matté <ult_combo at hotmail.com>
+Viktor Kojouharov <vkojouharov at gmail.com>
+Pawel Maruszczyk (http://hrabstwo.net)
+Pavel Selitskas <p.selitskas at gmail.com>
+Bjørn Johansen <post at bjornjohansen.no>
+Matthieu Penant <thieum22 at hotmail.com>
+Dominic Barnes <dominic at dbarnes.info>
+David Sullivan <david.sullivan at gmail.com>
+Thomas Jaggi <thomas at responsive.ch>
+Vahid Sohrabloo <vahid4134 at gmail.com>
+Travis Carden <travis.carden at gmail.com>
+Bruno M. Custódio <bruno at brunomcustodio.com>
+Nathanael Silverman <nathanael.silverman at gmail.com>
+Christian Wenz <christian at wenz.org>
+Steve Urmston <steve at urm.st>
+Zaven Muradyan <megalivoithos at gmail.com>
+Woody Gilk <shadowhand at deviantart.com>
+Zbigniew Motyka <zbigniew.motyka at gmail.com>
+Suhail Alkowaileet <xsoh.k7 at gmail.com>
+Toshi MARUYAMA <marutosijp2 at yahoo.co.jp>
+David Hansen <hansede at gmail.com>
+Brian Grinstead <briangrinstead at gmail.com>
+Christian Klammer <christian314159 at gmail.com>
+Steven Luscher <jquerycla at steveluscher.com>
+Gan Eng Chin <engchin.gan at gmail.com>
+Gabriel Schulhof <gabriel.schulhof at intel.com>
+Alexander Schmitz <arschmitz at gmail.com>
+Vilhjálmur Skúlason <vis at dmm.is>
+Siebrand Mazeland <siebrand at kitano.nl>
+Mohsen Ekhtiari <mohsenekhtiari at yahoo.com>
+Pere Orga <gotrunks at gmail.com>
+Jasper de Groot <mail at ugomobi.com>
+Stephane Deschamps <stephane.deschamps at gmail.com>
+Jyoti Deka <dekajp at gmail.com>
+Andrei Picus <office.nightcrawler at gmail.com>
+Ondrej Novy <novy at ondrej.org>
+Jacob McCutcheon <jacob.mccutcheon at gmail.com>
+Monika Piotrowicz <monika.piotrowicz at gmail.com>
+Imants Horsts <imants.horsts at inbox.lv>
+Eric Dahl <eric.c.dahl at gmail.com>
+Dave Stein <dave at behance.com>
+Dylan Barrell <dylan at barrell.com>
+Daniel DeGroff <djdegroff at gmail.com>
+Michael Wiencek <mwtuea at gmail.com>
+Thomas Meyer <meyertee at gmail.com>
+Ruslan Yakhyaev <ruslan at ruslan.io>
+Brian J. Dowling <bjd-dev at simplicity.net>
+Ben Higgins <ben at extrahop.com>
+Yermo Lamers <yml at yml.com>
+Patrick Stapleton <github at gdi2290.com>
+Trisha Crowley <trisha.crowley at gmail.com>
+Usman Akeju <akeju00+github at gmail.com>
+Rodrigo Menezes <rod333 at gmail.com>
+Jacques Perrault <jacques_perrault at us.ibm.com>
+Frederik Elvhage <frederik.elvhage at googlemail.com>
+Will Holley <willholley at gmail.com>
+Uri Gilad <antishok at gmail.com>
+Richard Gibson <richard.gibson at gmail.com>
+Simen Bekkhus <sbekkhus91 at gmail.com>
+Chen Eshchar <eshcharc at gmail.com>
+Bruno Pérel <brunoperel at gmail.com>
+Mohammed Alshehri <m at dralshehri.com>
+Lisa Seacat DeLuca <ldeluca at us.ibm.com>
+Anne-Gaelle Colom <coloma at westminster.ac.uk>
+Adam Foster <slimfoster at gmail.com>
+Luke Page <luke.a.page at gmail.com>
+Daniel Owens <daniel at matchstickmixup.com>
+Michael Orchard <morchard at scottlogic.co.uk>
+Marcus Warren <marcus at envoke.com>
+Nils Heuermann <nils at world-of-scripts.de>
+Marco Ziech <marco at ziech.net>
+Patricia Juarez <patrixd at gmail.com>
+Ben Mosher <me at benmosher.com>
+Ablay Keldibek <atomio.ak at gmail.com>
+Thomas Applencourt <thomas.applencourt at irsamc.ups-tlse.fr>
+Jiabao Wu <jiabao.foss at gmail.com>
+Eric Lee Carraway <github at ericcarraway.com>
+Victor Homyakov <vkhomyackov at gmail.com>
+Myeongjin Lee <aranet100 at gmail.com>
+Liran Sharir <lsharir at gmail.com>
+Weston Ruter <weston at xwp.co>
+Mani Mishra <manimishra902 at gmail.com>
+Hannah Methvin <hannahmethvin at gmail.com>
+Leonardo Balter <leonardo.balter at gmail.com>
+Benjamin Albert <benjamin_a5 at yahoo.com>
+Michał Gołębiowski <m.goleb at gmail.com>
+Alyosha Pushak <alyosha.pushak at gmail.com>
+Fahad Ahmad <fahadahmad41 at hotmail.com>
+Matt Brundage <github at mattbrundage.com>
+Francesc Baeta <francesc.baeta at gmail.com>
+Piotr Baran <piotros at wp.pl>
+Mukul Hase <mukulhase at gmail.com>
+Konstantin Dinev <kdinev at mail.bw.edu>
+Rand Scullard <rand at randscullard.com>
+Dan Strohl <dan at wjcg.net>
+Maksim Ryzhikov <rv.maksim at gmail.com>
+Amine HADDAD <haddad at allegorie.tv>
+Amanpreet Singh <apsdehal at gmail.com>
+Alexey Balchunas <bleshik at gmail.com>
+Peter Kehl <peter.kehl at gmail.com>
+Peter Dave Hello <hsu at peterdavehello.org>
+Johannes Schäfer <johnschaefer at gmx.de>
+Ville Skyttä <ville.skytta at iki.fi>
+Ryan Oriecuia <ryan.oriecuia at visioncritical.com>
diff --git a/inst/www/shared/jqueryui/LICENSE.txt b/inst/www/shared/jqueryui/LICENSE.txt
new file mode 100644
index 0000000..4819e54
--- /dev/null
+++ b/inst/www/shared/jqueryui/LICENSE.txt
@@ -0,0 +1,43 @@
+Copyright jQuery Foundation and other contributors, https://jquery.org/
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/jquery/jquery-ui
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+====
+
+Copyright and related rights for sample code are waived via CC0. Sample
+code is defined as all source code contained within the demos directory.
+
+CC0: http://creativecommons.org/publicdomain/zero/1.0/
+
+====
+
+All files located in the node_modules and external directories are
+externally maintained libraries used by this software which have their
+own licenses; we recommend you read them, as their terms may differ from
+the terms above.
diff --git a/inst/www/shared/jqueryui/README b/inst/www/shared/jqueryui/README
index 9961a11..c834659 100644
--- a/inst/www/shared/jqueryui/README
+++ b/inst/www/shared/jqueryui/README
@@ -1,8 +1,5 @@
-This a jQuery UI custom build, downloaded from:
-http://jqueryui.com/download/#!version=1.11.4&components=1111111111110111111111111111111111111
-
-It includes all components except the datepicker, because it conflicts with
-bootstrap-datepicker that is packaged with Shiny.
+This a full jQuery UI build, downloaded from:
+https://jqueryui.com/resources/download/jquery-ui-1.12.1.zip
 
 The copy of jQuery that is bundled with the download, under external/, is not
 included because Shiny already has its own copy of jQuery.
diff --git a/inst/www/shared/jqueryui/index.html b/inst/www/shared/jqueryui/index.html
index 7f4a59a..1796b1c 100644
--- a/inst/www/shared/jqueryui/index.html
+++ b/inst/www/shared/jqueryui/index.html
@@ -6,7 +6,7 @@
 	<link href="jquery-ui.css" rel="stylesheet">
 	<style>
 	body{
-		font: 62.5% "Trebuchet MS", sans-serif;
+		font-family: "Trebuchet MS", sans-serif;
 		margin: 50px;
 	}
 	.demoHeaders {
@@ -83,6 +83,12 @@
 <!-- Button -->
 <h2 class="demoHeaders">Button</h2>
 <button id="button">A button element</button>
+<button id="button-icon">An icon-only button</button>
+
+
+
+<!-- Checkboxradio -->
+<h2 class="demoHeaders">Checkboxradio</h2>
 <form style="margin-top: 1em;">
 	<div id="radioset">
 		<input type="radio" id="radio1" name="radio"><label for="radio1">Choice 1</label>
@@ -93,6 +99,34 @@
 
 
 
+<!-- Controlgroup -->
+<h2 class="demoHeaders">Controlgroup</h2>
+<fieldset>
+	<legend>Rental Car</legend>
+	<div id="controlgroup">
+		<select id="car-type">
+			<option>Compact car</option>
+			<option>Midsize car</option>
+			<option>Full size car</option>
+			<option>SUV</option>
+			<option>Luxury</option>
+			<option>Truck</option>
+			<option>Van</option>
+		</select>
+		<label for="transmission-standard">Standard</label>
+		<input type="radio" name="transmission" id="transmission-standard">
+		<label for="transmission-automatic">Automatic</label>
+		<input type="radio" name="transmission" id="transmission-automatic">
+		<label for="insurance">Insurance</label>
+		<input type="checkbox" name="insurance" id="insurance">
+		<label for="horizontal-spinner" class="ui-controlgroup-label"># of cars</label>
+		<input id="horizontal-spinner" class="ui-spinner-input">
+		<button>Book Now!</button>
+	</div>
+</fieldset>
+
+
+
 <!-- Tabs -->
 <h2 class="demoHeaders">Tabs</h2>
 <div id="tabs">
@@ -108,20 +142,21 @@
 
 
 
-<!-- Dialog NOTE: Dialog is not generated by UI in this demo so it can be visually styled in themeroller-->
 <h2 class="demoHeaders">Dialog</h2>
-<p><a href="#" id="dialog-link" class="ui-state-default ui-corner-all"><span class="ui-icon ui-icon-newwin"></span>Open Dialog</a></p>
+<p>
+	<button id="dialog-link" class="ui-button ui-corner-all ui-widget">
+		<span class="ui-icon ui-icon-newwin"></span>Open Dialog
+	</button>
+</p>
 
-<h2 class="demoHeaders">Overlay and Shadow Classes <em>(not currently used in UI widgets)</em></h2>
+<h2 class="demoHeaders">Overlay and Shadow Classes</h2>
 <div style="position: relative; width: 96%; height: 200px; padding:1% 2%; overflow:hidden;" class="fakewindowcontain">
 	<p>Lorem ipsum dolor sit amet,  Nulla nec tortor. Donec id elit quis purus consectetur consequat. </p><p>Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. Aliquam ante. Suspendisse scelerisque dui nec velit. Duis augue augue, gravida euismod, vulputate ac, facilisis id, sem. Morbi in orci. </p><p>Nulla purus lacus, pulvinar vel, malesuada ac, mattis nec, quam. Nam molestie scelerisque quam. Nullam feugiat cursus lacus.orem ipsum dolor sit a [...]
 
 	<!-- ui-dialog -->
-	<div class="ui-overlay"><div class="ui-widget-overlay"></div><div class="ui-widget-shadow ui-corner-all" style="width: 302px; height: 152px; position: absolute; left: 50px; top: 30px;"></div></div>
-	<div style="position: absolute; width: 280px; height: 130px;left: 50px; top: 30px; padding: 10px;" class="ui-widget ui-widget-content ui-corner-all">
-		<div class="ui-dialog-content ui-widget-content" style="background: none; border: 0;">
-			<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
-		</div>
+	<div class="ui-widget-overlay ui-front"></div>
+	<div style="position: absolute; width: 320px; left: 50px; top: 30px; padding: 1.2em" class="ui-widget ui-front ui-widget-content ui-corner-all ui-widget-shadow">
+		Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
 	</div>
 
 </div>
@@ -135,16 +170,16 @@
 
 <h2 class="demoHeaders">Framework Icons (content color preview)</h2>
 <ul id="icons" class="ui-widget ui-helper-clearfix">
-	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-n"><span class="ui-icon ui-icon-carat-1-n"></span></li>
-	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-ne"><span class="ui-icon ui-icon-carat-1-ne"></span></li>
-	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-e"><span class="ui-icon ui-icon-carat-1-e"></span></li>
-	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-se"><span class="ui-icon ui-icon-carat-1-se"></span></li>
-	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-s"><span class="ui-icon ui-icon-carat-1-s"></span></li>
-	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-sw"><span class="ui-icon ui-icon-carat-1-sw"></span></li>
-	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-w"><span class="ui-icon ui-icon-carat-1-w"></span></li>
-	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-nw"><span class="ui-icon ui-icon-carat-1-nw"></span></li>
-	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-2-n-s"><span class="ui-icon ui-icon-carat-2-n-s"></span></li>
-	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-2-e-w"><span class="ui-icon ui-icon-carat-2-e-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-n"><span class="ui-icon ui-icon-caret-1-n"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-ne"><span class="ui-icon ui-icon-caret-1-ne"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-e"><span class="ui-icon ui-icon-caret-1-e"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-se"><span class="ui-icon ui-icon-caret-1-se"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-s"><span class="ui-icon ui-icon-caret-1-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-sw"><span class="ui-icon ui-icon-caret-1-sw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-w"><span class="ui-icon ui-icon-caret-1-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-nw"><span class="ui-icon ui-icon-caret-1-nw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-2-n-s"><span class="ui-icon ui-icon-caret-2-n-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-2-e-w"><span class="ui-icon ui-icon-caret-2-e-w"></span></li>
 	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-n"><span class="ui-icon ui-icon-triangle-1-n"></span></li>
 	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-ne"><span class="ui-icon ui-icon-triangle-1-ne"></span></li>
 	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-e"><span class="ui-icon ui-icon-triangle-1-e"></span></li>
@@ -317,6 +352,10 @@
 
 
 
+<!-- Datepicker -->
+<h2 class="demoHeaders">Datepicker</h2>
+<div id="datepicker"></div>
+
 
 
 <!-- Progressbar -->
@@ -346,19 +385,19 @@
 <!-- Menu -->
 <h2 class="demoHeaders">Menu</h2>
 <ul style="width:100px;" id="menu">
-	<li>Item 1</li>
-	<li>Item 2</li>
-	<li>Item 3
+	<li><div>Item 1</div></li>
+	<li><div>Item 2</div></li>
+	<li><div>Item 3</div>
 		<ul>
-			<li>Item 3-1</li>
-			<li>Item 3-2</li>
-			<li>Item 3-3</li>
-			<li>Item 3-4</li>
-			<li>Item 3-5</li>
+			<li><div>Item 3-1</div></li>
+			<li><div>Item 3-2</div></li>
+			<li><div>Item 3-3</div></li>
+			<li><div>Item 3-4</div></li>
+			<li><div>Item 3-5</div></li>
 		</ul>
 	</li>
-	<li>Item 4</li>
-	<li>Item 5</li>
+	<li><div>Item 4</div></li>
+	<li><div>Item 5</div></li>
 </ul>
 
 
@@ -426,10 +465,21 @@ $( "#autocomplete" ).autocomplete({
 
 
 $( "#button" ).button();
+$( "#button-icon" ).button({
+	icon: "ui-icon-gear",
+	showLabel: false
+});
+
+
+
 $( "#radioset" ).buttonset();
 
 
 
+$( "#controlgroup" ).controlgroup();
+
+
+
 $( "#tabs" ).tabs();
 
 
@@ -461,6 +511,10 @@ $( "#dialog-link" ).click(function( event ) {
 
 
 
+$( "#datepicker" ).datepicker({
+	inline: true
+});
+
 
 
 $( "#slider" ).slider({
diff --git a/inst/www/shared/jqueryui/jquery-ui.structure.css b/inst/www/shared/jqueryui/jquery-ui.structure.css
index 842410e..9e6955a 100644
--- a/inst/www/shared/jqueryui/jquery-ui.structure.css
+++ b/inst/www/shared/jqueryui/jquery-ui.structure.css
@@ -1,5 +1,5 @@
 /*!
- * jQuery UI CSS Framework 1.11.4
+ * jQuery UI CSS Framework 1.12.1
  * http://jqueryui.com
  *
  * Copyright jQuery Foundation and other contributors
@@ -8,7 +8,6 @@
  *
  * http://api.jqueryui.com/category/theming/
  */
-
 /* Layout helpers
 ----------------------------------*/
 .ui-helper-hidden {
@@ -43,9 +42,6 @@
 .ui-helper-clearfix:after {
 	clear: both;
 }
-.ui-helper-clearfix {
-	min-height: 0; /* support: IE7 */
-}
 .ui-helper-zfix {
 	width: 100%;
 	height: 100%;
@@ -65,20 +61,27 @@
 ----------------------------------*/
 .ui-state-disabled {
 	cursor: default !important;
+	pointer-events: none;
 }
 
 
 /* Icons
 ----------------------------------*/
-
-/* states and images */
 .ui-icon {
-	display: block;
+	display: inline-block;
+	vertical-align: middle;
+	margin-top: -.25em;
+	position: relative;
 	text-indent: -99999px;
 	overflow: hidden;
 	background-repeat: no-repeat;
 }
 
+.ui-widget-icon-block {
+	left: 50%;
+	margin-left: -8px;
+	display: block;
+}
 
 /* Misc visuals
 ----------------------------------*/
@@ -91,114 +94,14 @@
 	width: 100%;
 	height: 100%;
 }
-.ui-draggable-handle {
-	-ms-touch-action: none;
-	touch-action: none;
-}
-.ui-resizable {
-	position: relative;
-}
-.ui-resizable-handle {
-	position: absolute;
-	font-size: 0.1px;
-	display: block;
-	-ms-touch-action: none;
-	touch-action: none;
-}
-.ui-resizable-disabled .ui-resizable-handle,
-.ui-resizable-autohide .ui-resizable-handle {
-	display: none;
-}
-.ui-resizable-n {
-	cursor: n-resize;
-	height: 7px;
-	width: 100%;
-	top: -5px;
-	left: 0;
-}
-.ui-resizable-s {
-	cursor: s-resize;
-	height: 7px;
-	width: 100%;
-	bottom: -5px;
-	left: 0;
-}
-.ui-resizable-e {
-	cursor: e-resize;
-	width: 7px;
-	right: -5px;
-	top: 0;
-	height: 100%;
-}
-.ui-resizable-w {
-	cursor: w-resize;
-	width: 7px;
-	left: -5px;
-	top: 0;
-	height: 100%;
-}
-.ui-resizable-se {
-	cursor: se-resize;
-	width: 12px;
-	height: 12px;
-	right: 1px;
-	bottom: 1px;
-}
-.ui-resizable-sw {
-	cursor: sw-resize;
-	width: 9px;
-	height: 9px;
-	left: -5px;
-	bottom: -5px;
-}
-.ui-resizable-nw {
-	cursor: nw-resize;
-	width: 9px;
-	height: 9px;
-	left: -5px;
-	top: -5px;
-}
-.ui-resizable-ne {
-	cursor: ne-resize;
-	width: 9px;
-	height: 9px;
-	right: -5px;
-	top: -5px;
-}
-.ui-selectable {
-	-ms-touch-action: none;
-	touch-action: none;
-}
-.ui-selectable-helper {
-	position: absolute;
-	z-index: 100;
-	border: 1px dotted black;
-}
-.ui-sortable-handle {
-	-ms-touch-action: none;
-	touch-action: none;
-}
 .ui-accordion .ui-accordion-header {
 	display: block;
 	cursor: pointer;
 	position: relative;
 	margin: 2px 0 0 0;
 	padding: .5em .5em .5em .7em;
-	min-height: 0; /* support: IE7 */
 	font-size: 100%;
 }
-.ui-accordion .ui-accordion-icons {
-	padding-left: 2.2em;
-}
-.ui-accordion .ui-accordion-icons .ui-accordion-icons {
-	padding-left: 2.2em;
-}
-.ui-accordion .ui-accordion-header .ui-accordion-header-icon {
-	position: absolute;
-	left: .5em;
-	top: 50%;
-	margin-top: -8px;
-}
 .ui-accordion .ui-accordion-content {
 	padding: 1em 2.2em;
 	border-top: 0;
@@ -210,17 +113,78 @@
 	left: 0;
 	cursor: default;
 }
+.ui-menu {
+	list-style: none;
+	padding: 0;
+	margin: 0;
+	display: block;
+	outline: 0;
+}
+.ui-menu .ui-menu {
+	position: absolute;
+}
+.ui-menu .ui-menu-item {
+	margin: 0;
+	cursor: pointer;
+	/* support: IE10, see #8844 */
+	list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
+}
+.ui-menu .ui-menu-item-wrapper {
+	position: relative;
+	padding: 3px 1em 3px .4em;
+}
+.ui-menu .ui-menu-divider {
+	margin: 5px 0;
+	height: 0;
+	font-size: 0;
+	line-height: 0;
+	border-width: 1px 0 0 0;
+}
+.ui-menu .ui-state-focus,
+.ui-menu .ui-state-active {
+	margin: -1px;
+}
+
+/* icon support */
+.ui-menu-icons {
+	position: relative;
+}
+.ui-menu-icons .ui-menu-item-wrapper {
+	padding-left: 2em;
+}
+
+/* left-aligned */
+.ui-menu .ui-icon {
+	position: absolute;
+	top: 0;
+	bottom: 0;
+	left: .2em;
+	margin: auto 0;
+}
+
+/* right-aligned */
+.ui-menu .ui-menu-icon {
+	left: auto;
+	right: 0;
+}
 .ui-button {
+	padding: .4em 1em;
 	display: inline-block;
 	position: relative;
-	padding: 0;
 	line-height: normal;
 	margin-right: .1em;
 	cursor: pointer;
 	vertical-align: middle;
 	text-align: center;
-	overflow: visible; /* removes extra width in IE */
+	-webkit-user-select: none;
+	-moz-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
+
+	/* Support: IE <= 11 */
+	overflow: visible;
 }
+
 .ui-button,
 .ui-button:link,
 .ui-button:visited,
@@ -228,94 +192,306 @@
 .ui-button:active {
 	text-decoration: none;
 }
+
 /* to make room for the icon, a width needs to be set here */
 .ui-button-icon-only {
-	width: 2.2em;
+	width: 2em;
+	box-sizing: border-box;
+	text-indent: -9999px;
+	white-space: nowrap;
 }
-/* button elements seem to need a little more width */
-button.ui-button-icon-only {
-	width: 2.4em;
+
+/* no icon support for input elements */
+input.ui-button.ui-button-icon-only {
+	text-indent: 0;
 }
-.ui-button-icons-only {
-	width: 3.4em;
+
+/* button icon element(s) */
+.ui-button-icon-only .ui-icon {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	margin-top: -8px;
+	margin-left: -8px;
 }
-button.ui-button-icons-only {
-	width: 3.7em;
+
+.ui-button.ui-icon-notext .ui-icon {
+	padding: 0;
+	width: 2.1em;
+	height: 2.1em;
+	text-indent: -9999px;
+	white-space: nowrap;
+
 }
 
-/* button text element */
-.ui-button .ui-button-text {
+input.ui-button.ui-icon-notext .ui-icon {
+	width: auto;
+	height: auto;
+	text-indent: 0;
+	white-space: normal;
+	padding: .4em 1em;
+}
+
+/* workarounds */
+/* Support: Firefox 5 - 40 */
+input.ui-button::-moz-focus-inner,
+button.ui-button::-moz-focus-inner {
+	border: 0;
+	padding: 0;
+}
+.ui-controlgroup {
+	vertical-align: middle;
+	display: inline-block;
+}
+.ui-controlgroup > .ui-controlgroup-item {
+	float: left;
+	margin-left: 0;
+	margin-right: 0;
+}
+.ui-controlgroup > .ui-controlgroup-item:focus,
+.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus {
+	z-index: 9999;
+}
+.ui-controlgroup-vertical > .ui-controlgroup-item {
 	display: block;
-	line-height: normal;
+	float: none;
+	width: 100%;
+	margin-top: 0;
+	margin-bottom: 0;
+	text-align: left;
+}
+.ui-controlgroup-vertical .ui-controlgroup-item {
+	box-sizing: border-box;
 }
-.ui-button-text-only .ui-button-text {
+.ui-controlgroup .ui-controlgroup-label {
 	padding: .4em 1em;
 }
-.ui-button-icon-only .ui-button-text,
-.ui-button-icons-only .ui-button-text {
-	padding: .4em;
-	text-indent: -9999999px;
+.ui-controlgroup .ui-controlgroup-label span {
+	font-size: 80%;
 }
-.ui-button-text-icon-primary .ui-button-text,
-.ui-button-text-icons .ui-button-text {
-	padding: .4em 1em .4em 2.1em;
+.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item {
+	border-left: none;
 }
-.ui-button-text-icon-secondary .ui-button-text,
-.ui-button-text-icons .ui-button-text {
-	padding: .4em 2.1em .4em 1em;
+.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item {
+	border-top: none;
 }
-.ui-button-text-icons .ui-button-text {
-	padding-left: 2.1em;
-	padding-right: 2.1em;
+.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content {
+	border-right: none;
 }
-/* no icon support for input elements, provide padding by default */
-input.ui-button {
-	padding: .4em 1em;
+.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content {
+	border-bottom: none;
 }
 
-/* button icon element(s) */
-.ui-button-icon-only .ui-icon,
-.ui-button-text-icon-primary .ui-icon,
-.ui-button-text-icon-secondary .ui-icon,
-.ui-button-text-icons .ui-icon,
-.ui-button-icons-only .ui-icon {
+/* Spinner specific style fixes */
+.ui-controlgroup-vertical .ui-spinner-input {
+
+	/* Support: IE8 only, Android < 4.4 only */
+	width: 75%;
+	width: calc( 100% - 2.4em );
+}
+.ui-controlgroup-vertical .ui-spinner .ui-spinner-up {
+	border-top-style: solid;
+}
+
+.ui-checkboxradio-label .ui-icon-background {
+	box-shadow: inset 1px 1px 1px #ccc;
+	border-radius: .12em;
+	border: none;
+}
+.ui-checkboxradio-radio-label .ui-icon-background {
+	width: 16px;
+	height: 16px;
+	border-radius: 1em;
+	overflow: visible;
+	border: none;
+}
+.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,
+.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon {
+	background-image: none;
+	width: 8px;
+	height: 8px;
+	border-width: 4px;
+	border-style: solid;
+}
+.ui-checkboxradio-disabled {
+	pointer-events: none;
+}
+.ui-datepicker {
+	width: 17em;
+	padding: .2em .2em 0;
+	display: none;
+}
+.ui-datepicker .ui-datepicker-header {
+	position: relative;
+	padding: .2em 0;
+}
+.ui-datepicker .ui-datepicker-prev,
+.ui-datepicker .ui-datepicker-next {
 	position: absolute;
-	top: 50%;
-	margin-top: -8px;
+	top: 2px;
+	width: 1.8em;
+	height: 1.8em;
 }
-.ui-button-icon-only .ui-icon {
+.ui-datepicker .ui-datepicker-prev-hover,
+.ui-datepicker .ui-datepicker-next-hover {
+	top: 1px;
+}
+.ui-datepicker .ui-datepicker-prev {
+	left: 2px;
+}
+.ui-datepicker .ui-datepicker-next {
+	right: 2px;
+}
+.ui-datepicker .ui-datepicker-prev-hover {
+	left: 1px;
+}
+.ui-datepicker .ui-datepicker-next-hover {
+	right: 1px;
+}
+.ui-datepicker .ui-datepicker-prev span,
+.ui-datepicker .ui-datepicker-next span {
+	display: block;
+	position: absolute;
 	left: 50%;
 	margin-left: -8px;
+	top: 50%;
+	margin-top: -8px;
 }
-.ui-button-text-icon-primary .ui-button-icon-primary,
-.ui-button-text-icons .ui-button-icon-primary,
-.ui-button-icons-only .ui-button-icon-primary {
-	left: .5em;
+.ui-datepicker .ui-datepicker-title {
+	margin: 0 2.3em;
+	line-height: 1.8em;
+	text-align: center;
+}
+.ui-datepicker .ui-datepicker-title select {
+	font-size: 1em;
+	margin: 1px 0;
+}
+.ui-datepicker select.ui-datepicker-month,
+.ui-datepicker select.ui-datepicker-year {
+	width: 45%;
+}
+.ui-datepicker table {
+	width: 100%;
+	font-size: .9em;
+	border-collapse: collapse;
+	margin: 0 0 .4em;
+}
+.ui-datepicker th {
+	padding: .7em .3em;
+	text-align: center;
+	font-weight: bold;
+	border: 0;
+}
+.ui-datepicker td {
+	border: 0;
+	padding: 1px;
+}
+.ui-datepicker td span,
+.ui-datepicker td a {
+	display: block;
+	padding: .2em;
+	text-align: right;
+	text-decoration: none;
+}
+.ui-datepicker .ui-datepicker-buttonpane {
+	background-image: none;
+	margin: .7em 0 0 0;
+	padding: 0 .2em;
+	border-left: 0;
+	border-right: 0;
+	border-bottom: 0;
 }
-.ui-button-text-icon-secondary .ui-button-icon-secondary,
-.ui-button-text-icons .ui-button-icon-secondary,
-.ui-button-icons-only .ui-button-icon-secondary {
-	right: .5em;
+.ui-datepicker .ui-datepicker-buttonpane button {
+	float: right;
+	margin: .5em .2em .4em;
+	cursor: pointer;
+	padding: .2em .6em .3em .6em;
+	width: auto;
+	overflow: visible;
+}
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
+	float: left;
 }
 
-/* button sets */
-.ui-buttonset {
-	margin-right: 7px;
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi {
+	width: auto;
 }
-.ui-buttonset .ui-button {
-	margin-left: 0;
-	margin-right: -.3em;
+.ui-datepicker-multi .ui-datepicker-group {
+	float: left;
+}
+.ui-datepicker-multi .ui-datepicker-group table {
+	width: 95%;
+	margin: 0 auto .4em;
+}
+.ui-datepicker-multi-2 .ui-datepicker-group {
+	width: 50%;
+}
+.ui-datepicker-multi-3 .ui-datepicker-group {
+	width: 33.3%;
+}
+.ui-datepicker-multi-4 .ui-datepicker-group {
+	width: 25%;
+}
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
+	border-left-width: 0;
+}
+.ui-datepicker-multi .ui-datepicker-buttonpane {
+	clear: left;
+}
+.ui-datepicker-row-break {
+	clear: both;
+	width: 100%;
+	font-size: 0;
 }
 
-/* workarounds */
-/* reset extra padding in Firefox, see h5bp.com/l */
-input.ui-button::-moz-focus-inner,
-button.ui-button::-moz-focus-inner {
-	border: 0;
-	padding: 0;
+/* RTL support */
+.ui-datepicker-rtl {
+	direction: rtl;
 }
-.ui-dialog {
+.ui-datepicker-rtl .ui-datepicker-prev {
+	right: 2px;
+	left: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-next {
+	left: 2px;
+	right: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-prev:hover {
+	right: 1px;
+	left: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-next:hover {
+	left: 1px;
+	right: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane {
+	clear: right;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane button {
+	float: left;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
+.ui-datepicker-rtl .ui-datepicker-group {
+	float: right;
+}
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
+	border-right-width: 0;
+	border-left-width: 1px;
+}
+
+/* Icons */
+.ui-datepicker .ui-icon {
+	display: block;
+	text-indent: -99999px;
 	overflow: hidden;
+	background-repeat: no-repeat;
+	left: .5em;
+	top: .3em;
+}
+.ui-dialog {
 	position: absolute;
 	top: 0;
 	left: 0;
@@ -364,68 +540,121 @@ button.ui-button::-moz-focus-inner {
 	margin: .5em .4em .5em 0;
 	cursor: pointer;
 }
+.ui-dialog .ui-resizable-n {
+	height: 2px;
+	top: 0;
+}
+.ui-dialog .ui-resizable-e {
+	width: 2px;
+	right: 0;
+}
+.ui-dialog .ui-resizable-s {
+	height: 2px;
+	bottom: 0;
+}
+.ui-dialog .ui-resizable-w {
+	width: 2px;
+	left: 0;
+}
+.ui-dialog .ui-resizable-se,
+.ui-dialog .ui-resizable-sw,
+.ui-dialog .ui-resizable-ne,
+.ui-dialog .ui-resizable-nw {
+	width: 7px;
+	height: 7px;
+}
 .ui-dialog .ui-resizable-se {
-	width: 12px;
-	height: 12px;
-	right: -5px;
-	bottom: -5px;
-	background-position: 16px 16px;
+	right: 0;
+	bottom: 0;
+}
+.ui-dialog .ui-resizable-sw {
+	left: 0;
+	bottom: 0;
+}
+.ui-dialog .ui-resizable-ne {
+	right: 0;
+	top: 0;
+}
+.ui-dialog .ui-resizable-nw {
+	left: 0;
+	top: 0;
 }
 .ui-draggable .ui-dialog-titlebar {
 	cursor: move;
 }
-.ui-menu {
-	list-style: none;
-	padding: 0;
-	margin: 0;
-	display: block;
-	outline: none;
-}
-.ui-menu .ui-menu {
-	position: absolute;
+.ui-draggable-handle {
+	-ms-touch-action: none;
+	touch-action: none;
 }
-.ui-menu .ui-menu-item {
+.ui-resizable {
 	position: relative;
-	margin: 0;
-	padding: 3px 1em 3px .4em;
-	cursor: pointer;
-	min-height: 0; /* support: IE7 */
-	/* support: IE10, see #8844 */
-	list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
 }
-.ui-menu .ui-menu-divider {
-	margin: 5px 0;
-	height: 0;
-	font-size: 0;
-	line-height: 0;
-	border-width: 1px 0 0 0;
+.ui-resizable-handle {
+	position: absolute;
+	font-size: 0.1px;
+	display: block;
+	-ms-touch-action: none;
+	touch-action: none;
 }
-.ui-menu .ui-state-focus,
-.ui-menu .ui-state-active {
-	margin: -1px;
+.ui-resizable-disabled .ui-resizable-handle,
+.ui-resizable-autohide .ui-resizable-handle {
+	display: none;
 }
-
-/* icon support */
-.ui-menu-icons {
-	position: relative;
+.ui-resizable-n {
+	cursor: n-resize;
+	height: 7px;
+	width: 100%;
+	top: -5px;
+	left: 0;
 }
-.ui-menu-icons .ui-menu-item {
-	padding-left: 2em;
+.ui-resizable-s {
+	cursor: s-resize;
+	height: 7px;
+	width: 100%;
+	bottom: -5px;
+	left: 0;
 }
-
-/* left-aligned */
-.ui-menu .ui-icon {
-	position: absolute;
+.ui-resizable-e {
+	cursor: e-resize;
+	width: 7px;
+	right: -5px;
 	top: 0;
-	bottom: 0;
-	left: .2em;
-	margin: auto 0;
+	height: 100%;
 }
-
-/* right-aligned */
-.ui-menu .ui-menu-icon {
-	left: auto;
-	right: 0;
+.ui-resizable-w {
+	cursor: w-resize;
+	width: 7px;
+	left: -5px;
+	top: 0;
+	height: 100%;
+}
+.ui-resizable-se {
+	cursor: se-resize;
+	width: 12px;
+	height: 12px;
+	right: 1px;
+	bottom: 1px;
+}
+.ui-resizable-sw {
+	cursor: sw-resize;
+	width: 9px;
+	height: 9px;
+	left: -5px;
+	bottom: -5px;
+}
+.ui-resizable-nw {
+	cursor: nw-resize;
+	width: 9px;
+	height: 9px;
+	left: -5px;
+	top: -5px;
+}
+.ui-resizable-ne {
+	cursor: ne-resize;
+	width: 9px;
+	height: 9px;
+	right: -5px;
+	top: -5px;
 }
 .ui-progressbar {
 	height: 2em;
@@ -445,6 +674,15 @@ button.ui-button::-moz-focus-inner {
 .ui-progressbar-indeterminate .ui-progressbar-value {
 	background-image: none;
 }
+.ui-selectable {
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-selectable-helper {
+	position: absolute;
+	z-index: 100;
+	border: 1px dotted black;
+}
 .ui-selectmenu-menu {
 	padding: 0;
 	margin: 0;
@@ -455,7 +693,6 @@ button.ui-button::-moz-focus-inner {
 }
 .ui-selectmenu-menu .ui-menu {
 	overflow: auto;
-	/* Support: IE7 */
 	overflow-x: hidden;
 	padding-bottom: 1px;
 }
@@ -471,28 +708,20 @@ button.ui-button::-moz-focus-inner {
 .ui-selectmenu-open {
 	display: block;
 }
-.ui-selectmenu-button {
-	display: inline-block;
-	overflow: hidden;
-	position: relative;
-	text-decoration: none;
-	cursor: pointer;
-}
-.ui-selectmenu-button span.ui-icon {
-	right: 0.5em;
-	left: auto;
-	margin-top: -8px;
-	position: absolute;
-	top: 50%;
-}
-.ui-selectmenu-button span.ui-selectmenu-text {
-	text-align: left;
-	padding: 0.4em 2.1em 0.4em 1em;
+.ui-selectmenu-text {
 	display: block;
-	line-height: 1.4;
+	margin-right: 20px;
 	overflow: hidden;
 	text-overflow: ellipsis;
+}
+.ui-selectmenu-button.ui-button {
+	text-align: left;
 	white-space: nowrap;
+	width: 14em;
+}
+.ui-selectmenu-icon.ui-icon {
+	float: right;
+	margin-top: 0;
 }
 .ui-slider {
 	position: relative;
@@ -559,6 +788,10 @@ button.ui-button::-moz-focus-inner {
 .ui-slider-vertical .ui-slider-range-max {
 	top: 0;
 }
+.ui-sortable-handle {
+	-ms-touch-action: none;
+	touch-action: none;
+}
 .ui-spinner {
 	position: relative;
 	display: inline-block;
@@ -570,14 +803,14 @@ button.ui-button::-moz-focus-inner {
 	border: none;
 	background: none;
 	color: inherit;
-	padding: 0;
+	padding: .222em 0;
 	margin: .2em 0;
 	vertical-align: middle;
 	margin-left: .4em;
-	margin-right: 22px;
+	margin-right: 2em;
 }
 .ui-spinner-button {
-	width: 16px;
+	width: 1.6em;
 	height: 50%;
 	font-size: .5em;
 	padding: 0;
@@ -591,16 +824,9 @@ button.ui-button::-moz-focus-inner {
 }
 /* more specificity required here to override default borders */
 .ui-spinner a.ui-spinner-button {
-	border-top: none;
-	border-bottom: none;
-	border-right: none;
-}
-/* vertically center icon */
-.ui-spinner .ui-icon {
-	position: absolute;
-	margin-top: -8px;
-	top: 50%;
-	left: 0;
+	border-top-style: none;
+	border-bottom-style: none;
+	border-right-style: none;
 }
 .ui-spinner-up {
 	top: 0;
@@ -608,12 +834,6 @@ button.ui-button::-moz-focus-inner {
 .ui-spinner-down {
 	bottom: 0;
 }
-
-/* TR overrides */
-.ui-spinner .ui-icon-triangle-1-s {
-	/* need to fix icons sprite */
-	background-position: -65px -16px;
-}
 .ui-tabs {
 	position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
 	padding: .2em;
@@ -660,8 +880,6 @@ button.ui-button::-moz-focus-inner {
 	position: absolute;
 	z-index: 9999;
 	max-width: 300px;
-	-webkit-box-shadow: 0 0 5px #aaa;
-	box-shadow: 0 0 5px #aaa;
 }
 body .ui-tooltip {
 	border-width: 2px;
diff --git a/inst/www/shared/jqueryui/jquery-ui.structure.min.css b/inst/www/shared/jqueryui/jquery-ui.structure.min.css
index 8956c85..1e80c4d 100644
--- a/inst/www/shared/jqueryui/jquery-ui.structure.min.css
+++ b/inst/www/shared/jqueryui/jquery-ui.structure.min.css
@@ -1,5 +1,5 @@
-/*! jQuery UI - v1.11.4 - 2016-01-05
+/*! jQuery UI - v1.12.1 - 2016-09-14
 * http://jqueryui.com
 * Copyright jQuery Foundation and other contributors; Licensed MIT */
 
-.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;heig [...]
\ No newline at end of file
+.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:abs [...]
\ No newline at end of file
diff --git a/inst/www/shared/jqueryui/jquery-ui.theme.css b/inst/www/shared/jqueryui/jquery-ui.theme.css
index 998a19d..6089438 100644
--- a/inst/www/shared/jqueryui/jquery-ui.theme.css
+++ b/inst/www/shared/jqueryui/jquery-ui.theme.css
@@ -1,5 +1,5 @@
 /*!
- * jQuery UI CSS Framework 1.11.4
+ * jQuery UI CSS Framework 1.12.1
  * http://jqueryui.com
  *
  * Copyright jQuery Foundation and other contributors
@@ -8,7 +8,7 @@
  *
  * http://api.jqueryui.com/category/theming/
  *
- * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&fwDefault=normal&cornerRadius=3px&bgColorHeader=e9e9e9&bgTextureHeader=flat&borderColorHeader=dddddd&fcHeader=333333&iconColorHeader=444444&bgColorContent=ffffff&bgTextureContent=flat&borderColorContent=dddddd&fcContent=333333&iconColorContent=444444&bgColorDefault=f6f6f6&bgTextureDefault=flat&borderColorDefault=c5c5c5&fcDefault=454545&iconColorDefault=777777&bg [...]
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/?bgShadowXPos=&bgOverlayXPos=&bgErrorXPos=&bgHighlightXPos=&bgContentXPos=&bgHeaderXPos=&bgActiveXPos=&bgHoverXPos=&bgDefaultXPos=&bgShadowYPos=&bgOverlayYPos=&bgErrorYPos=&bgHighlightYPos=&bgContentYPos=&bgHeaderYPos=&bgActiveYPos=&bgHoverYPos=&bgDefaultYPos=&bgShadowRepeat=&bgOverlayRepeat=&bgErrorRepeat=&bgHighlightRepeat=&bgContentRepeat=&bgHeaderRepeat=&bgActiveRepeat=&bgHoverRepeat=&bgDefaultRepeat=&iconsHover= [...]
  */
 
 
@@ -28,6 +28,9 @@
 	font-family: Arial,Helvetica,sans-serif;
 	font-size: 1em;
 }
+.ui-widget.ui-widget-content {
+	border: 1px solid #c5c5c5;
+}
 .ui-widget-content {
 	border: 1px solid #dddddd;
 	background: #ffffff;
@@ -50,7 +53,13 @@
 ----------------------------------*/
 .ui-state-default,
 .ui-widget-content .ui-state-default,
-.ui-widget-header .ui-state-default {
+.ui-widget-header .ui-state-default,
+.ui-button,
+
+/* We use html here because we need a greater specificity to make sure disabled
+works properly when clicked or hovered */
+html .ui-button.ui-state-disabled:hover,
+html .ui-button.ui-state-disabled:active {
 	border: 1px solid #c5c5c5;
 	background: #f6f6f6;
 	font-weight: normal;
@@ -58,7 +67,11 @@
 }
 .ui-state-default a,
 .ui-state-default a:link,
-.ui-state-default a:visited {
+.ui-state-default a:visited,
+a.ui-button,
+a:link.ui-button,
+a:visited.ui-button,
+.ui-button {
 	color: #454545;
 	text-decoration: none;
 }
@@ -67,7 +80,9 @@
 .ui-widget-header .ui-state-hover,
 .ui-state-focus,
 .ui-widget-content .ui-state-focus,
-.ui-widget-header .ui-state-focus {
+.ui-widget-header .ui-state-focus,
+.ui-button:hover,
+.ui-button:focus {
 	border: 1px solid #cccccc;
 	background: #ededed;
 	font-weight: normal;
@@ -80,18 +95,32 @@
 .ui-state-focus a,
 .ui-state-focus a:hover,
 .ui-state-focus a:link,
-.ui-state-focus a:visited {
+.ui-state-focus a:visited,
+a.ui-button:hover,
+a.ui-button:focus {
 	color: #2b2b2b;
 	text-decoration: none;
 }
+
+.ui-visual-focus {
+	box-shadow: 0 0 3px 1px rgb(94, 158, 214);
+}
 .ui-state-active,
 .ui-widget-content .ui-state-active,
-.ui-widget-header .ui-state-active {
+.ui-widget-header .ui-state-active,
+a.ui-button:active,
+.ui-button:active,
+.ui-button.ui-state-active:hover {
 	border: 1px solid #003eff;
 	background: #007fff;
 	font-weight: normal;
 	color: #ffffff;
 }
+.ui-icon-background,
+.ui-state-active .ui-icon-background {
+	border: #003eff;
+	background-color: #ffffff;
+}
 .ui-state-active a,
 .ui-state-active a:link,
 .ui-state-active a:visited {
@@ -108,6 +137,10 @@
 	background: #fffa90;
 	color: #777620;
 }
+.ui-state-checked {
+	border: 1px solid #dad55e;
+	background: #fffa90;
+}
 .ui-state-highlight a,
 .ui-widget-content .ui-state-highlight a,
 .ui-widget-header .ui-state-highlight a {
@@ -168,41 +201,45 @@
 .ui-widget-header .ui-icon {
 	background-image: url("images/ui-icons_444444_256x240.png");
 }
-.ui-state-default .ui-icon {
-	background-image: url("images/ui-icons_777777_256x240.png");
-}
 .ui-state-hover .ui-icon,
-.ui-state-focus .ui-icon {
+.ui-state-focus .ui-icon,
+.ui-button:hover .ui-icon,
+.ui-button:focus .ui-icon {
 	background-image: url("images/ui-icons_555555_256x240.png");
 }
-.ui-state-active .ui-icon {
+.ui-state-active .ui-icon,
+.ui-button:active .ui-icon {
 	background-image: url("images/ui-icons_ffffff_256x240.png");
 }
-.ui-state-highlight .ui-icon {
+.ui-state-highlight .ui-icon,
+.ui-button .ui-state-highlight.ui-icon {
 	background-image: url("images/ui-icons_777620_256x240.png");
 }
 .ui-state-error .ui-icon,
 .ui-state-error-text .ui-icon {
 	background-image: url("images/ui-icons_cc0000_256x240.png");
 }
+.ui-button .ui-icon {
+	background-image: url("images/ui-icons_777777_256x240.png");
+}
 
 /* positioning */
 .ui-icon-blank { background-position: 16px 16px; }
-.ui-icon-carat-1-n { background-position: 0 0; }
-.ui-icon-carat-1-ne { background-position: -16px 0; }
-.ui-icon-carat-1-e { background-position: -32px 0; }
-.ui-icon-carat-1-se { background-position: -48px 0; }
-.ui-icon-carat-1-s { background-position: -64px 0; }
-.ui-icon-carat-1-sw { background-position: -80px 0; }
-.ui-icon-carat-1-w { background-position: -96px 0; }
-.ui-icon-carat-1-nw { background-position: -112px 0; }
-.ui-icon-carat-2-n-s { background-position: -128px 0; }
-.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-caret-1-n { background-position: 0 0; }
+.ui-icon-caret-1-ne { background-position: -16px 0; }
+.ui-icon-caret-1-e { background-position: -32px 0; }
+.ui-icon-caret-1-se { background-position: -48px 0; }
+.ui-icon-caret-1-s { background-position: -65px 0; }
+.ui-icon-caret-1-sw { background-position: -80px 0; }
+.ui-icon-caret-1-w { background-position: -96px 0; }
+.ui-icon-caret-1-nw { background-position: -112px 0; }
+.ui-icon-caret-2-n-s { background-position: -128px 0; }
+.ui-icon-caret-2-e-w { background-position: -144px 0; }
 .ui-icon-triangle-1-n { background-position: 0 -16px; }
 .ui-icon-triangle-1-ne { background-position: -16px -16px; }
 .ui-icon-triangle-1-e { background-position: -32px -16px; }
 .ui-icon-triangle-1-se { background-position: -48px -16px; }
-.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-s { background-position: -65px -16px; }
 .ui-icon-triangle-1-sw { background-position: -80px -16px; }
 .ui-icon-triangle-1-w { background-position: -96px -16px; }
 .ui-icon-triangle-1-nw { background-position: -112px -16px; }
@@ -212,7 +249,7 @@
 .ui-icon-arrow-1-ne { background-position: -16px -32px; }
 .ui-icon-arrow-1-e { background-position: -32px -32px; }
 .ui-icon-arrow-1-se { background-position: -48px -32px; }
-.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-s { background-position: -65px -32px; }
 .ui-icon-arrow-1-sw { background-position: -80px -32px; }
 .ui-icon-arrow-1-w { background-position: -96px -32px; }
 .ui-icon-arrow-1-nw { background-position: -112px -32px; }
@@ -224,7 +261,7 @@
 .ui-icon-arrowstop-1-e { background-position: -208px -32px; }
 .ui-icon-arrowstop-1-s { background-position: -224px -32px; }
 .ui-icon-arrowstop-1-w { background-position: -240px -32px; }
-.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-n { background-position: 1px -48px; }
 .ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
 .ui-icon-arrowthick-1-e { background-position: -32px -48px; }
 .ui-icon-arrowthick-1-se { background-position: -48px -48px; }
@@ -397,14 +434,10 @@
 /* Overlays */
 .ui-widget-overlay {
 	background: #aaaaaa;
-	opacity: .3;
-	filter: Alpha(Opacity=30); /* support: IE8 */
+	opacity: .003;
+	filter: Alpha(Opacity=.3); /* support: IE8 */
 }
 .ui-widget-shadow {
-	margin: 0px 0 0 0px;
-	padding: 5px;
-	background: #666666;
-	opacity: .3;
-	filter: Alpha(Opacity=30); /* support: IE8 */
-	border-radius: 8px;
+	-webkit-box-shadow: 0px 0px 5px #666666;
+	box-shadow: 0px 0px 5px #666666;
 }
diff --git a/inst/www/shared/jqueryui/jquery-ui.theme.min.css b/inst/www/shared/jqueryui/jquery-ui.theme.min.css
index f43d108..d2d4f6a 100644
--- a/inst/www/shared/jqueryui/jquery-ui.theme.min.css
+++ b/inst/www/shared/jqueryui/jquery-ui.theme.min.css
@@ -1,5 +1,5 @@
-/*! jQuery UI - v1.11.4 - 2016-01-05
+/*! jQuery UI - v1.12.1 - 2016-09-14
 * http://jqueryui.com
 * Copyright jQuery Foundation and other contributors; Licensed MIT */
 
-.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-d [...]
\ No newline at end of file
+.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color [...]
\ No newline at end of file
diff --git a/inst/www/shared/shiny-showcase.js b/inst/www/shared/shiny-showcase.js
index f91f70b..a2ecb89 100644
--- a/inst/www/shared/shiny-showcase.js
+++ b/inst/www/shared/shiny-showcase.js
@@ -54,13 +54,13 @@
           }
         }
       }
-      // If this is not a text node, descend recursively to see how many 
+      // If this is not a text node, descend recursively to see how many
       // lines it contains.
       else if (child.nodeType === 1) { // ELEMENT_NODE
         var ret = findTextPoint(child, line - newlines, col);
         if (ret.element !== null)
-          return ret; 
-        else 
+          return ret;
+        else
           newlines += ret.offset;
       }
     }
@@ -85,7 +85,7 @@
       var code = document.getElementById(srcfile.replace(/\./g, "_") + "_code");
       var start = findTextPoint(code, ref[0], ref[4]);
       var end = findTextPoint(code, ref[2], ref[5]);
-      
+
       // If the insertion point can't be found, bail out now
       if (start.element === null || end.element === null)
          return;
@@ -129,10 +129,10 @@
     var animateCodeMs = animate ? animateMs : 1;
 
     // set the source and targets for the tab move
-    var newHostElement = above ? 
+    var newHostElement = above ?
       document.getElementById("showcase-sxs-code") :
       document.getElementById("showcase-code-inline");
-    var currentHostElement = above ? 
+    var currentHostElement = above ?
       document.getElementById("showcase-code-inline") :
       document.getElementById("showcase-sxs-code");
 
@@ -162,7 +162,7 @@
 
       $(newHostElement).fadeIn();
       if (!above) {
-        // remove the applied width and zoom on the app container, and 
+        // remove the applied width and zoom on the app container, and
         // scroll smoothly down to the code's new home
         document.getElementById("showcase-app-container").removeAttribute("style");
         if (animate)
@@ -234,9 +234,9 @@
   };
 
   // make the code scrollable to about the height of the browser, less space
-  // for the tabs 
+  // for the tabs
   var setCodeHeightFromDocHeight = function() {
-    document.getElementById("showcase-code-content").style.height = 
+    document.getElementById("showcase-code-content").style.height =
       $(window).height() + "px";
   };
 
@@ -247,7 +247,7 @@
       // IE8 puts the content of <script> tags into innerHTML but
       // not innerText
       var content = mdContent.innerText || mdContent.innerHTML;
-      document.getElementById("readme-md").innerHTML = 
+      document.getElementById("readme-md").innerHTML =
         (new Showdown.converter()).makeHtml(content)
     }
   }
diff --git a/inst/www/shared/shiny.css b/inst/www/shared/shiny.css
index a9e07c1..e612702 100644
--- a/inst/www/shared/shiny.css
+++ b/inst/www/shared/shiny.css
@@ -1,14 +1,68 @@
-body.disconnected {
+#shiny-disconnected-overlay {
+  position: fixed;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
   background-color: #999;
   opacity: 0.5;
+  overflow: hidden;
+  z-index: 99998;
+  pointer-events: none;
 }
 
-table.data {
-  width: auto;
+.table.shiny-table > thead > tr > th,
+.table.shiny-table > tbody > tr > th,
+.table.shiny-table > tfoot > tr > th,
+.table.shiny-table > thead > tr > td,
+.table.shiny-table > tbody > tr > td,
+.table.shiny-table > tfoot > tr > td {
+  padding-left: 12px;
+  padding-right: 12px ;
 }
-table.data td[align=right] {
-  font-family: monospace;
-  text-align: right;
+
+.shiny-table.spacing-xs > thead > tr > th,
+.shiny-table.spacing-xs > tbody > tr > th,
+.shiny-table.spacing-xs > tfoot > tr > th,
+.shiny-table.spacing-xs > thead > tr > td,
+.shiny-table.spacing-xs > tbody > tr > td,
+.shiny-table.spacing-xs > tfoot > tr > td {
+  padding-top: 3px;
+  padding-bottom: 3px;
+}
+
+.shiny-table.spacing-s > thead > tr > th,
+.shiny-table.spacing-s > tbody > tr > th,
+.shiny-table.spacing-s > tfoot > tr > th,
+.shiny-table.spacing-s > thead > tr > td,
+.shiny-table.spacing-s > tbody > tr > td,
+.shiny-table.spacing-s > tfoot > tr > td {
+  padding-top: 5px;
+  padding-bottom: 5px;
+}
+
+.shiny-table.spacing-m > thead > tr > th,
+.shiny-table.spacing-m > tbody > tr > th,
+.shiny-table.spacing-m > tfoot > tr > th,
+.shiny-table.spacing-m > thead > tr > td,
+.shiny-table.spacing-m > tbody > tr > td,
+.shiny-table.spacing-m > tfoot > tr > td {
+  padding-top: 8px;
+  padding-bottom: 8px;
+}
+
+.shiny-table.spacing-l > thead > tr > th,
+.shiny-table.spacing-l > tbody > tr > th,
+.shiny-table.spacing-l > tfoot > tr > th,
+.shiny-table.spacing-l > thead > tr > td,
+.shiny-table.spacing-l > tbody > tr > td,
+.shiny-table.spacing-l > tfoot > tr > td {
+  padding-top: 10px;
+  padding-bottom: 10px;
+}
+
+.shiny-table .NA {
+  color: #909090;
 }
 
 .shiny-output-error {
@@ -70,6 +124,7 @@ table.data td[align=right] {
   max-width: 100%;
 }
 
+/* Old-style progress */
 .shiny-progress-container {
   position: fixed;
   top: 0px;
@@ -113,6 +168,21 @@ table.data td[align=right] {
   font-size: 80%;
 }
 
+/* New-style progress (uses notifications API) */
+.shiny-progress-notification .progress {
+  margin-bottom: 5px;
+  height: 10px;
+}
+
+.shiny-progress-notification .progress-text .progress-message {
+  font-weight: bold;
+  font-size: 90%;
+}
+
+.shiny-progress-notification .progress-text .progress-detail {
+  font-size: 80%;
+}
+
 .crosshair {
   cursor: crosshair;
 }
@@ -222,3 +292,63 @@ table.data td[align=right] {
 .shiny-input-container > div > select:not(.selectized) {
   width: 100%;
 }
+
+
+#shiny-notification-panel {
+  position: fixed;
+  bottom: 0;
+  right: 0;
+  background-color: rgba(0,0,0,0);
+  padding: 2px;
+  width: 250px;
+  z-index: 99999;
+}
+
+.shiny-notification {
+  background-color: #e8e8e8;
+  color: #333;
+  border: 1px solid #ccc;
+  border-radius: 3px;
+  opacity: 0.85;
+  padding: 10px 8px 10px 10px;
+  margin: 2px;
+}
+
+.shiny-notification-message {
+  color: #31708f;
+  background-color: #d9edf7;
+  border: 1px solid #bce8f1;
+}
+
+.shiny-notification-warning {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+  border: 1px solid #faebcc;
+}
+
+.shiny-notification-error {
+  color: #a94442;
+  background-color: #f2dede;
+  border: 1px solid #ebccd1;
+}
+
+.shiny-notification-close {
+  float: right;
+  font-weight: bold;
+  font-size: 18px;
+  bottom: 9px;
+  position: relative;
+  padding-left: 4px;
+  color: #444;
+  cursor: default;
+}
+
+.shiny-notification-close:hover {
+  color: #000;
+}
+
+.shiny-notification-content-action a {
+  color: rgb(51, 122, 183);
+  text-decoration: underline;
+  font-weight: bold;
+}
diff --git a/inst/www/shared/shiny.js b/inst/www/shared/shiny.js
index 19f5583..6225d27 100644
--- a/inst/www/shared/shiny.js
+++ b/inst/www/shared/shiny.js
@@ -1,4952 +1,5352 @@
+'use strict';
+
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
+
 //---------------------------------------------------------------------
 // Source file: ../srcjs/_start.js
 
-/*jshint
-  undef:true,
-  browser:true,
-  devel: true,
-  jquery:true,
-  strict:false,
-  curly:false,
-  indent:2
-*/
-/* global strftime */
-
-(function() {
+(function () {
   var $ = jQuery;
 
   var exports = window.Shiny = window.Shiny || {};
 
-
-  $(document).on('submit', 'form:not([action])', function(e) {
+  $(document).on('submit', 'form:not([action])', function (e) {
     e.preventDefault();
   });
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/utils.js
-
-function escapeHTML(str) {
-  return str.replace(/&/g, "&")
-            .replace(/</g, "<")
-            .replace(/>/g, ">")
-            .replace(/"/g, """)
-            .replace(/'/g, "'")
-            .replace(/\//g,"&#x2F;");
-}
-
-function randomId() {
-  return Math.floor(0x100000000 + (Math.random() * 0xF00000000)).toString(16);
-}
-
-function strToBool(str) {
-  if (!str || !str.toLowerCase)
-    return undefined;
-
-  switch(str.toLowerCase()) {
-    case 'true':
-      return true;
-    case 'false':
-      return false;
-    default:
-      return undefined;
-  }
-}
-
-// A wrapper for getComputedStyle that is compatible with older browsers.
-// This is significantly faster than jQuery's .css() function.
-function getStyle(el, styleProp) {
-  var x;
-  if (el.currentStyle)
-    x = el.currentStyle[styleProp];
-  else if (window.getComputedStyle) {
-    // getComputedStyle can return null when we're inside a hidden iframe on
-    // Firefox; don't attempt to retrieve style props in this case.
-    // https://bugzilla.mozilla.org/show_bug.cgi?id=548397
-    var style = document.defaultView.getComputedStyle(el, null);
-    if (style)
-      x = style.getPropertyValue(styleProp);
-  }
-  return x;
-}
-
-// Convert a number to a string with leading zeros
-function padZeros(n, digits) {
-  var str = n.toString();
-  while (str.length < digits)
-    str = "0" + str;
-  return str;
-}
-
-// Take a string with format "YYYY-MM-DD" and return a Date object.
-// IE8 and QTWebKit don't support YYYY-MM-DD, but they support YYYY/MM/DD
-function parseDate(dateString) {
-  var date = new Date(dateString);
-  if (isNaN(date))
-    date = new Date(dateString.replace(/-/g, "/"));
-  return date;
-}
-
-// Given a Date object, return a string in yyyy-mm-dd format, using the
-// UTC date. This may be a day off from the date in the local time zone.
-function formatDateUTC(date) {
-  if (date instanceof Date) {
-    return date.getUTCFullYear() + '-' +
-           padZeros(date.getUTCMonth()+1, 2) + '-' +
-           padZeros(date.getUTCDate(), 2);
-
-  } else {
-    return null;
-  }
-}
-
-
-// Given an element and a function(width, height), returns a function(). When
-// the output function is called, it calls the input function with the offset
-// width and height of the input element--but only if the size of the element
-// is non-zero and the size is different than the last time the output
-// function was called.
-//
-// Basically we are trying to filter out extraneous calls to func, so that
-// when the window size changes or whatever, we don't run resize logic for
-// elements that haven't actually changed size or aren't visible anyway.
-function makeResizeFilter(el, func) {
-  var lastSize = {};
-  return function() {
-    var size = { w: el.offsetWidth, h: el.offsetHeight };
-    if (size.w === 0 && size.h === 0)
-      return;
-    if (size.w === lastSize.w && size.h === lastSize.h)
-      return;
-    lastSize = size;
-    func(size.w, size.h);
-  };
-}
-
-var _BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder ||
-    window.MozBlobBuilder || window.MSBlobBuilder;
-
-function makeBlob(parts) {
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/utils.js
 
-  // 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
+  function escapeHTML(str) {
+    return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\//g, "&#x2F;");
+  }
 
-  // Google Chrome 21.0.1180.81 on Xubuntu 12.04:
-  // Has Blob constructor, accepts ArrayBufferView args, accepts ArrayBuffer
-  // but with a deprecation warning message
+  function randomId() {
+    return Math.floor(0x100000000 + Math.random() * 0xF00000000).toString(16);
+  }
 
-  // Firefox 15.0 on Xubuntu 12.04:
-  // Has Blob constructor, accepts both ArrayBuffer and ArrayBufferView args
+  function strToBool(str) {
+    if (!str || !str.toLowerCase) return undefined;
 
-  // Chromium 18.0.1025.168 (Developer Build 134367 Linux) on Xubuntu 12.04:
-  // No Blob constructor. Has WebKitBlobBuilder.
+    switch (str.toLowerCase()) {
+      case 'true':
+        return true;
+      case 'false':
+        return false;
+      default:
+        return undefined;
+    }
+  }
 
-  try {
-    return new Blob(parts);
+  // A wrapper for getComputedStyle that is compatible with older browsers.
+  // This is significantly faster than jQuery's .css() function.
+  function getStyle(el, styleProp) {
+    var x;
+    if (el.currentStyle) x = el.currentStyle[styleProp];else if (window.getComputedStyle) {
+      // getComputedStyle can return null when we're inside a hidden iframe on
+      // Firefox; don't attempt to retrieve style props in this case.
+      // https://bugzilla.mozilla.org/show_bug.cgi?id=548397
+      var style = document.defaultView.getComputedStyle(el, null);
+      if (style) x = style.getPropertyValue(styleProp);
+    }
+    return x;
   }
-  catch (e) {
-    var blobBuilder = new _BlobBuilder();
-    $.each(parts, function(i, part) {
-      blobBuilder.append(part);
-    });
-    return blobBuilder.getBlob();
+
+  // Convert a number to a string with leading zeros
+  function padZeros(n, digits) {
+    var str = n.toString();
+    while (str.length < digits) {
+      str = "0" + str;
+    }return str;
   }
-}
-
-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";
-}
-
-function pixelRatio() {
-  if (window.devicePixelRatio) {
-    return window.devicePixelRatio;
-  } else {
-    return 1;
+
+  // Take a string with format "YYYY-MM-DD" and return a Date object.
+  // IE8 and QTWebKit don't support YYYY-MM-DD, but they support YYYY/MM/DD
+  function parseDate(dateString) {
+    var date = new Date(dateString);
+    if (isNaN(date)) date = new Date(dateString.replace(/-/g, "/"));
+    return date;
   }
-}
-
-// 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) {
-  /*jshint evil: true */
-  var func = new Function("with (this) {return (" + expr + ");}");
-  return function(scope) {
-    return func.call(scope);
-  };
-}
-
-function asArray(value) {
-  if (value === null || value === undefined)
-    return [];
-  if ($.isArray(value))
-    return value;
-  return [value];
-}
-
-// We need a stable sorting algorithm for ordering
-// bindings by priority and insertion order.
-function mergeSort(list, sortfunc) {
-  function merge(sortfunc, a, b) {
-    var ia = 0;
-    var ib = 0;
-    var sorted = [];
-    while (ia < a.length && ib < b.length) {
-      if (sortfunc(a[ia], b[ib]) <= 0) {
-        sorted.push(a[ia++]);
-      }
-      else {
-        sorted.push(b[ib++]);
-      }
+
+  // Given a Date object, return a string in yyyy-mm-dd format, using the
+  // UTC date. This may be a day off from the date in the local time zone.
+  function formatDateUTC(date) {
+    if (date instanceof Date) {
+      return date.getUTCFullYear() + '-' + padZeros(date.getUTCMonth() + 1, 2) + '-' + padZeros(date.getUTCDate(), 2);
+    } else {
+      return null;
     }
-    while (ia < a.length)
-      sorted.push(a[ia++]);
-    while (ib < b.length)
-      sorted.push(b[ib++]);
-    return sorted;
   }
 
-  // Don't mutate list argument
-  list = list.slice(0);
-
-  for (var chunkSize = 1; chunkSize < list.length; chunkSize *= 2) {
-    for (var i = 0; i < list.length; i += chunkSize * 2) {
-      var listA = list.slice(i, i + chunkSize);
-      var listB = list.slice(i + chunkSize, i + chunkSize * 2);
-      var merged = merge(sortfunc, listA, listB);
-      var args = [i, merged.length];
-      Array.prototype.push.apply(args, merged);
-      Array.prototype.splice.apply(list, args);
-    }
+  // Given an element and a function(width, height), returns a function(). When
+  // the output function is called, it calls the input function with the offset
+  // width and height of the input element--but only if the size of the element
+  // is non-zero and the size is different than the last time the output
+  // function was called.
+  //
+  // Basically we are trying to filter out extraneous calls to func, so that
+  // when the window size changes or whatever, we don't run resize logic for
+  // elements that haven't actually changed size or aren't visible anyway.
+  function makeResizeFilter(el, func) {
+    var lastSize = {};
+    return function () {
+      var size = { w: el.offsetWidth, h: el.offsetHeight };
+      if (size.w === 0 && size.h === 0) return;
+      if (size.w === lastSize.w && size.h === lastSize.h) return;
+      lastSize = size;
+      func(size.w, size.h);
+    };
   }
 
-  return list;
-}
+  var _BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;
 
-// Escape jQuery selector metacharacters: !"#$%&'()*+,./:;<=>?@[\]^`{|}~
-var $escape = exports.$escape = function(val) {
-  return val.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g, '\\$1');
-};
+  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.
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/browser.js
+    // 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
 
-var browser = (function() {
+    // Firefox 15.0 on Xubuntu 12.04:
+    // Has Blob constructor, accepts both ArrayBuffer and ArrayBufferView args
 
-  var isQt = false;
-  // For easy handling of Qt quirks using CSS
-  if (/\bQt\//.test(window.navigator.userAgent)) {
-    $(document.documentElement).addClass('qt');
-    isQt = true;
+    // 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();
+    }
   }
 
-  // Enable special treatment for Qt 5 quirks on Linux
-  if (/\bQt\/5/.test(window.navigator.userAgent) &&
-      /Linux/.test(window.navigator.userAgent)) {
-    $(document.documentElement).addClass('qt5');
+  function pixelRatio() {
+    if (window.devicePixelRatio) {
+      return window.devicePixelRatio;
+    } else {
+      return 1;
+    }
   }
 
-  // Detect IE information
-  var isIE = (navigator.appName === 'Microsoft Internet Explorer');
+  // 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) {
+    /*jshint evil: true */
+    var func = new Function("with (this) {return (" + expr + ");}");
+    return function (scope) {
+      return func.call(scope);
+    };
+  }
 
-  function getIEVersion() {
-    var rv = -1;
-    if (isIE) {
-      var ua = navigator.userAgent;
-      var re  = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})");
-      if (re.exec(ua) !== null)
-        rv = parseFloat(RegExp.$1);
-    }
-    return rv;
+  function asArray(value) {
+    if (value === null || value === undefined) return [];
+    if ($.isArray(value)) return value;
+    return [value];
   }
 
-  return {
-    isQt: isQt,
-    isIE: isIE,
-    IEVersion: getIEVersion()
-  };
+  // We need a stable sorting algorithm for ordering
+  // bindings by priority and insertion order.
+  function mergeSort(list, sortfunc) {
+    function merge(sortfunc, a, b) {
+      var ia = 0;
+      var ib = 0;
+      var sorted = [];
+      while (ia < a.length && ib < b.length) {
+        if (sortfunc(a[ia], b[ib]) <= 0) {
+          sorted.push(a[ia++]);
+        } else {
+          sorted.push(b[ib++]);
+        }
+      }
+      while (ia < a.length) {
+        sorted.push(a[ia++]);
+      }while (ib < b.length) {
+        sorted.push(b[ib++]);
+      }return sorted;
+    }
 
-})();
+    // Don't mutate list argument
+    list = list.slice(0);
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_rate.js
+    for (var chunkSize = 1; chunkSize < list.length; chunkSize *= 2) {
+      for (var i = 0; i < list.length; i += chunkSize * 2) {
+        var listA = list.slice(i, i + chunkSize);
+        var listB = list.slice(i + chunkSize, i + chunkSize * 2);
+        var merged = merge(sortfunc, listA, listB);
+        var args = [i, merged.length];
+        Array.prototype.push.apply(args, merged);
+        Array.prototype.splice.apply(list, args);
+      }
+    }
 
-var Invoker = function(target, func) {
-  this.target = target;
-  this.func = func;
-};
+    return list;
+  }
 
-(function() {
-  this.normalCall =
-  this.immediateCall = function() {
-    this.func.apply(this.target, arguments);
+  // Escape jQuery selector metacharacters: !"#$%&'()*+,./:;<=>?@[\]^`{|}~
+  var $escape = exports.$escape = function (val) {
+    return val.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g, '\\$1');
   };
-}).call(Invoker.prototype);
 
-var Debouncer = function(target, func, delayMs) {
-  this.target = target;
-  this.func = func;
-  this.delayMs = delayMs;
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/browser.js
 
-  this.timerId = null;
-  this.args = null;
-};
+  var browser = function () {
 
-(function() {
-  this.normalCall = function() {
-    var self = this;
+    var isQt = false;
+    // For easy handling of Qt quirks using CSS
+    if (/\bQt\//.test(window.navigator.userAgent)) {
+      $(document.documentElement).addClass('qt');
+      isQt = true;
+    }
 
-    this.$clearTimer();
-    this.args = arguments;
+    // Enable special treatment for Qt 5 quirks on Linux
+    if (/\bQt\/5/.test(window.navigator.userAgent) && /Linux/.test(window.navigator.userAgent)) {
+      $(document.documentElement).addClass('qt5');
+    }
 
-    this.timerId = setTimeout(function() {
-      // IE8 doesn't reliably clear timeout, so this additional
-      // check is needed
-      if (self.timerId === null)
-        return;
-      self.$clearTimer();
-      self.$invoke();
-    }, this.delayMs);
-  };
-  this.immediateCall = function() {
-    this.$clearTimer();
-    this.args = arguments;
-    this.$invoke();
-  };
-  this.isPending = function() {
-    return this.timerId !== null;
-  };
-  this.$clearTimer = function() {
-    if (this.timerId !== null) {
-      clearTimeout(this.timerId);
-      this.timerId = null;
+    // Detect IE information
+    var isIE = navigator.appName === 'Microsoft Internet Explorer';
+
+    function getIEVersion() {
+      var rv = -1;
+      if (isIE) {
+        var ua = navigator.userAgent;
+        var re = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})");
+        if (re.exec(ua) !== null) rv = parseFloat(RegExp.$1);
+      }
+      return rv;
     }
+
+    return {
+      isQt: isQt,
+      isIE: isIE,
+      IEVersion: getIEVersion()
+    };
+  }();
+
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_rate.js
+
+  var Invoker = function Invoker(target, func) {
+    this.target = target;
+    this.func = func;
   };
-  this.$invoke = function() {
-    this.func.apply(this.target, this.args);
+
+  (function () {
+    this.normalCall = this.immediateCall = function () {
+      this.func.apply(this.target, arguments);
+    };
+  }).call(Invoker.prototype);
+
+  var Debouncer = function Debouncer(target, func, delayMs) {
+    this.target = target;
+    this.func = func;
+    this.delayMs = delayMs;
+
+    this.timerId = null;
     this.args = null;
   };
-}).call(Debouncer.prototype);
 
-var Throttler = function(target, func, delayMs) {
-  this.target = target;
-  this.func = func;
-  this.delayMs = delayMs;
+  (function () {
+    this.normalCall = function () {
+      var self = this;
 
-  this.timerId = null;
-  this.args = null;
-};
+      this.$clearTimer();
+      this.args = arguments;
 
-(function() {
-  this.normalCall = function() {
-    var self = this;
-
-    this.args = arguments;
-    if (this.timerId === null) {
-      this.$invoke();
-      this.timerId = setTimeout(function() {
+      this.timerId = setTimeout(function () {
         // IE8 doesn't reliably clear timeout, so this additional
         // check is needed
-        if (self.timerId === null)
-          return;
+        if (self.timerId === null) return;
         self.$clearTimer();
-        if (self.args)
-          self.normalCall.apply(self, self.args);
+        self.$invoke();
       }, this.delayMs);
-    }
-  };
-  this.immediateCall = function() {
-    this.$clearTimer();
-    this.args = arguments;
-    this.$invoke();
-  };
-  this.isPending = function() {
-    return this.timerId !== null;
-  };
-  this.$clearTimer = function() {
-    if (this.timerId !== null) {
-      clearTimeout(this.timerId);
-      this.timerId = null;
-    }
-  };
-  this.$invoke = function() {
-    this.func.apply(this.target, this.args);
+    };
+    this.immediateCall = function () {
+      this.$clearTimer();
+      this.args = arguments;
+      this.$invoke();
+    };
+    this.isPending = function () {
+      return this.timerId !== null;
+    };
+    this.$clearTimer = function () {
+      if (this.timerId !== null) {
+        clearTimeout(this.timerId);
+        this.timerId = null;
+      }
+    };
+    this.$invoke = function () {
+      this.func.apply(this.target, this.args);
+      this.args = null;
+    };
+  }).call(Debouncer.prototype);
+
+  var Throttler = function Throttler(target, func, delayMs) {
+    this.target = target;
+    this.func = func;
+    this.delayMs = delayMs;
+
+    this.timerId = null;
     this.args = null;
   };
-}).call(Throttler.prototype);
-
-// Returns a debounced version of the given function.
-// Debouncing means that when the function is invoked,
-// there is a delay of `threshold` milliseconds before
-// it is actually executed, and if the function is
-// invoked again before that threshold has elapsed then
-// the clock starts over.
-//
-// For example, if a function is debounced with a
-// threshold of 1000ms, then calling it 17 times at
-// 900ms intervals will result in a single execution
-// of the underlying function, 1000ms after the 17th
-// call.
-function debounce(threshold, func) {
-  var timerId = null;
-  var self, args;
-  return function() {
-    self = this;
-    args = arguments;
-    if (timerId !== null) {
-      clearTimeout(timerId);
-      timerId = null;
-    }
-    timerId = setTimeout(function() {
-      // IE8 doesn't reliably clear timeout, so this additional
-      // check is needed
-      if (timerId === null)
-        return;
-      timerId = null;
-      func.apply(self, args);
-    }, threshold);
-  };
-}
-
-// Returns a throttled version of the given function.
-// Throttling means that the underlying function will
-// be executed no more than once every `threshold`
-// milliseconds.
-//
-// For example, if a function is throttled with a
-// threshold of 1000ms, then calling it 17 times at
-// 900ms intervals will result in something like 15
-// or 16 executions of the underlying function.
-function throttle(threshold, func) {
-  var executionPending = false;
-  var timerId = null;
-  var self, args;
-
-  function throttled() {
-    self = null;
-    args = null;
-    if (timerId === null) {
-      // Haven't seen a call recently. Execute now and
-      // start a timer to buffer any subsequent calls.
-      timerId = setTimeout(function() {
-        // When time expires, clear the timer; and if
-        // there has been a call in the meantime, repeat.
-        timerId = null;
-        if (executionPending) {
-          executionPending = false;
-          throttled.apply(self, args);
-        }
-      }, threshold);
-      func.apply(this, arguments);
-    }
-    else {
-      // Something executed recently. Don't do anything
-      // except set up target/arguments to be called later
-      executionPending = true;
+
+  (function () {
+    this.normalCall = function () {
+      var self = this;
+
+      this.args = arguments;
+      if (this.timerId === null) {
+        this.$invoke();
+        this.timerId = setTimeout(function () {
+          // IE8 doesn't reliably clear timeout, so this additional
+          // check is needed
+          if (self.timerId === null) return;
+          self.$clearTimer();
+          if (self.args) self.normalCall.apply(self, self.args);
+        }, this.delayMs);
+      }
+    };
+    this.immediateCall = function () {
+      this.$clearTimer();
+      this.args = arguments;
+      this.$invoke();
+    };
+    this.isPending = function () {
+      return this.timerId !== null;
+    };
+    this.$clearTimer = function () {
+      if (this.timerId !== null) {
+        clearTimeout(this.timerId);
+        this.timerId = null;
+      }
+    };
+    this.$invoke = function () {
+      this.func.apply(this.target, this.args);
+      this.args = null;
+    };
+  }).call(Throttler.prototype);
+
+  // Returns a debounced version of the given function.
+  // Debouncing means that when the function is invoked,
+  // there is a delay of `threshold` milliseconds before
+  // it is actually executed, and if the function is
+  // invoked again before that threshold has elapsed then
+  // the clock starts over.
+  //
+  // For example, if a function is debounced with a
+  // threshold of 1000ms, then calling it 17 times at
+  // 900ms intervals will result in a single execution
+  // of the underlying function, 1000ms after the 17th
+  // call.
+  function debounce(threshold, func) {
+    var timerId = null;
+    var self, args;
+    return function () {
       self = this;
       args = arguments;
-    }
+      if (timerId !== null) {
+        clearTimeout(timerId);
+        timerId = null;
+      }
+      timerId = setTimeout(function () {
+        // IE8 doesn't reliably clear timeout, so this additional
+        // check is needed
+        if (timerId === null) return;
+        timerId = null;
+        func.apply(self, args);
+      }, threshold);
+    };
   }
-  return throttled;
-}
-
-// Schedules data to be sent to shinyapp at the next setTimeout(0).
-// Batches multiple input calls into one websocket message.
-var InputBatchSender = function(shinyapp) {
-  this.shinyapp = shinyapp;
-  this.timerId = null;
-  this.pendingData = {};
-  this.reentrant = false;
-  this.lastChanceCallback = [];
-};
-(function() {
-  this.setInput = function(name, value) {
-    var self = this;
-
-    this.pendingData[name] = value;
-    if (!this.timerId && !this.reentrant) {
-      this.timerId = setTimeout(function() {
-        self.reentrant = true;
-        try {
-          $.each(self.lastChanceCallback, function(i, callback) {
-            callback();
-          });
-          self.timerId = null;
-          var currentData = self.pendingData;
-          self.pendingData = {};
-          self.shinyapp.sendInput(currentData);
-        } finally {
-          self.reentrant = false;
-        }
-      }, 0);
+
+  // Returns a throttled version of the given function.
+  // Throttling means that the underlying function will
+  // be executed no more than once every `threshold`
+  // milliseconds.
+  //
+  // For example, if a function is throttled with a
+  // threshold of 1000ms, then calling it 17 times at
+  // 900ms intervals will result in something like 15
+  // or 16 executions of the underlying function.
+  // eslint-disable-next-line no-unused-vars
+  function throttle(threshold, func) {
+    var executionPending = false;
+    var timerId = null;
+    var self, args;
+
+    function throttled() {
+      self = null;
+      args = null;
+      if (timerId === null) {
+        // Haven't seen a call recently. Execute now and
+        // start a timer to buffer any subsequent calls.
+        timerId = setTimeout(function () {
+          // When time expires, clear the timer; and if
+          // there has been a call in the meantime, repeat.
+          timerId = null;
+          if (executionPending) {
+            executionPending = false;
+            throttled.apply(self, args);
+          }
+        }, threshold);
+        func.apply(this, arguments);
+      } else {
+        // Something executed recently. Don't do anything
+        // except set up target/arguments to be called later
+        executionPending = true;
+        self = this;
+        args = arguments;
+      }
     }
+    return throttled;
+  }
+
+  // Schedules data to be sent to shinyapp at the next setTimeout(0).
+  // Batches multiple input calls into one websocket message.
+  var InputBatchSender = function InputBatchSender(shinyapp) {
+    this.shinyapp = shinyapp;
+    this.timerId = null;
+    this.pendingData = {};
+    this.reentrant = false;
+    this.lastChanceCallback = [];
   };
-}).call(InputBatchSender.prototype);
-
-var InputNoResendDecorator = function(target, initialValues) {
-  this.target = target;
-  this.lastSentValues = initialValues || {};
-};
-(function() {
-  this.setInput = function(name, value) {
-    var jsonValue = JSON.stringify(value);
-    if (this.lastSentValues[name] === jsonValue)
-      return;
-    this.lastSentValues[name] = jsonValue;
-    this.target.setInput(name, value);
-  };
-  this.reset = function(values) {
-    values = values || {};
-    var strValues = {};
-    $.each(values, function(key, value) {
-      strValues[key] = JSON.stringify(value);
-    });
-    this.lastSentValues = strValues;
+  (function () {
+    this.setInput = function (name, value) {
+      var self = this;
+
+      this.pendingData[name] = value;
+      if (!this.timerId && !this.reentrant) {
+        this.timerId = setTimeout(function () {
+          self.reentrant = true;
+          try {
+            $.each(self.lastChanceCallback, function (i, callback) {
+              callback();
+            });
+            self.timerId = null;
+            var currentData = self.pendingData;
+            self.pendingData = {};
+            self.shinyapp.sendInput(currentData);
+          } finally {
+            self.reentrant = false;
+          }
+        }, 0);
+      }
+    };
+  }).call(InputBatchSender.prototype);
+
+  var InputNoResendDecorator = function InputNoResendDecorator(target, initialValues) {
+    this.target = target;
+    this.lastSentValues = initialValues || {};
   };
-}).call(InputNoResendDecorator.prototype);
-
-var InputDeferDecorator = function(target) {
-  this.target = target;
-  this.pendingInput = {};
-};
-(function() {
-  this.setInput = function(name, value) {
-    if (/^\./.test(name))
+  (function () {
+    this.setInput = function (name, value) {
+      var jsonValue = JSON.stringify(value);
+      if (this.lastSentValues[name] === jsonValue) return;
+      this.lastSentValues[name] = jsonValue;
       this.target.setInput(name, value);
-    else
-      this.pendingInput[name] = value;
-  };
-  this.submit = function() {
-    for (var name in this.pendingInput) {
-      if (this.pendingInput.hasOwnProperty(name))
-        this.target.setInput(name, this.pendingInput[name]);
-    }
-  };
-}).call(InputDeferDecorator.prototype);
-
-var InputEventDecorator = function(target) {
-  this.target = target;
-};
-(function() {
-  this.setInput = function(name, value, immediate) {
-    var evt = jQuery.Event("shiny:inputchanged");
-    var name2 = name.split(':');
-    evt.name = name2[0];
-    evt.inputType = name2.length > 1 ? name2[1] : '';
-    evt.value = value;
-    $(document).trigger(evt);
-    if (!evt.isDefaultPrevented()) {
-      name = evt.name;
-      if (evt.inputType !== '') name += ':' + evt.inputType;
-      this.target.setInput(name, evt.value, immediate);
-    }
-  };
-}).call(InputEventDecorator.prototype);
-
-var InputRateDecorator = function(target) {
-  this.target = target;
-  this.inputRatePolicies = {};
-};
-(function() {
-  this.setInput = function(name, value, immediate) {
-    this.$ensureInit(name);
-    if (immediate)
-      this.inputRatePolicies[name].immediateCall(name, value, immediate);
-    else
-      this.inputRatePolicies[name].normalCall(name, value, immediate);
-  };
-  this.setRatePolicy = function(name, mode, millis) {
-    if (mode === 'direct') {
-      this.inputRatePolicies[name] = new Invoker(this, this.$doSetInput);
-    }
-    else if (mode === 'debounce') {
-      this.inputRatePolicies[name] = new Debouncer(this, this.$doSetInput, millis);
-    }
-    else if (mode === 'throttle') {
-      this.inputRatePolicies[name] = new Throttler(this, this.$doSetInput, millis);
-    }
+    };
+    this.reset = function (values) {
+      values = values || {};
+      var strValues = {};
+      $.each(values, function (key, value) {
+        strValues[key] = JSON.stringify(value);
+      });
+      this.lastSentValues = strValues;
+    };
+  }).call(InputNoResendDecorator.prototype);
+
+  var InputDeferDecorator = function InputDeferDecorator(target) {
+    this.target = target;
+    this.pendingInput = {};
   };
-  this.$ensureInit = function(name) {
-    if (!(name in this.inputRatePolicies))
-      this.setRatePolicy(name, 'direct');
+  (function () {
+    this.setInput = function (name, value) {
+      if (/^\./.test(name)) this.target.setInput(name, value);else this.pendingInput[name] = value;
+    };
+    this.submit = function () {
+      for (var name in this.pendingInput) {
+        if (this.pendingInput.hasOwnProperty(name)) this.target.setInput(name, this.pendingInput[name]);
+      }
+    };
+  }).call(InputDeferDecorator.prototype);
+
+  var InputEventDecorator = function InputEventDecorator(target) {
+    this.target = target;
   };
-  this.$doSetInput = function(name, value) {
-    this.target.setInput(name, value);
+  (function () {
+    this.setInput = function (name, value, immediate) {
+      var evt = jQuery.Event("shiny:inputchanged");
+      var name2 = name.split(':');
+      evt.name = name2[0];
+      evt.inputType = name2.length > 1 ? name2[1] : '';
+      evt.value = value;
+      $(document).trigger(evt);
+      if (!evt.isDefaultPrevented()) {
+        name = evt.name;
+        if (evt.inputType !== '') name += ':' + evt.inputType;
+        this.target.setInput(name, evt.value, immediate);
+      }
+    };
+  }).call(InputEventDecorator.prototype);
+
+  var InputRateDecorator = function InputRateDecorator(target) {
+    this.target = target;
+    this.inputRatePolicies = {};
   };
-}).call(InputRateDecorator.prototype);
+  (function () {
+    this.setInput = function (name, value, immediate) {
+      this.$ensureInit(name);
+      if (immediate) this.inputRatePolicies[name].immediateCall(name, value, immediate);else this.inputRatePolicies[name].normalCall(name, value, immediate);
+    };
+    this.setRatePolicy = function (name, mode, millis) {
+      if (mode === 'direct') {
+        this.inputRatePolicies[name] = new Invoker(this, this.$doSetInput);
+      } else if (mode === 'debounce') {
+        this.inputRatePolicies[name] = new Debouncer(this, this.$doSetInput, millis);
+      } else if (mode === 'throttle') {
+        this.inputRatePolicies[name] = new Throttler(this, this.$doSetInput, millis);
+      }
+    };
+    this.$ensureInit = function (name) {
+      if (!(name in this.inputRatePolicies)) this.setRatePolicy(name, 'direct');
+    };
+    this.$doSetInput = function (name, value) {
+      this.target.setInput(name, value);
+    };
+  }).call(InputRateDecorator.prototype);
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/shinyapp.js
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/shinyapp.js
 
-var ShinyApp = function() {
-  this.$socket = null;
+  var ShinyApp = function ShinyApp() {
+    this.$socket = null;
 
-  // Cached input values
-  this.$inputValues = {};
+    // Cached input values
+    this.$inputValues = {};
 
-  // Output bindings
-  this.$bindings = {};
+    // Output bindings
+    this.$bindings = {};
 
-  // Cached values/errors
-  this.$values = {};
-  this.$errors = {};
+    // Cached values/errors
+    this.$values = {};
+    this.$errors = {};
 
-  // Conditional bindings (show/hide element based on expression)
-  this.$conditionals = {};
+    // Conditional bindings (show/hide element based on expression)
+    this.$conditionals = {};
 
-  this.$pendingMessages = [];
-  this.$activeRequests = {};
-  this.$nextRequestId = 0;
-};
+    this.$pendingMessages = [];
+    this.$activeRequests = {};
+    this.$nextRequestId = 0;
 
-(function() {
+    this.$allowReconnect = false;
+  };
 
-  this.connect = function(initialInput) {
-    if (this.$socket)
-      throw "Connect was already called on this application object";
+  (function () {
 
-    $.extend(initialInput, {
-      // IE8 and IE9 have some limitations with data URIs
-      ".clientdata_allowDataUriScheme": typeof WebSocket !== 'undefined'
-    });
+    this.connect = function (initialInput) {
+      if (this.$socket) throw "Connect was already called on this application object";
 
-    this.$socket = this.createSocket();
-    this.$initialInput = initialInput;
-    $.extend(this.$inputValues, initialInput);
+      $.extend(initialInput, {
+        // IE8 and IE9 have some limitations with data URIs
+        ".clientdata_allowDataUriScheme": typeof WebSocket !== 'undefined'
+      });
 
-    this.$updateConditionals();
-  };
+      this.$socket = this.createSocket();
+      this.$initialInput = initialInput;
+      $.extend(this.$inputValues, initialInput);
 
-  this.isConnected = function() {
-    return !!this.$socket;
-  };
+      this.$updateConditionals();
+    };
+
+    this.isConnected = function () {
+      return !!this.$socket;
+    };
+
+    var scheduledReconnect = null;
+    this.reconnect = function () {
+      // This function can be invoked directly even if there's a scheduled
+      // reconnect, so be sure to clear any such scheduled reconnects.
+      clearTimeout(scheduledReconnect);
+
+      if (this.isConnected()) throw "Attempted to reconnect, but already connected.";
+
+      this.$socket = this.createSocket();
+      this.$initialInput = $.extend({}, this.$inputValues);
+      this.$updateConditionals();
+    };
+
+    this.createSocket = function () {
+      var self = this;
+
+      var createSocketFunc = exports.createSocket || function () {
+        var protocol = 'ws:';
+        if (window.location.protocol === 'https:') protocol = 'wss:';
 
-  this.createSocket = function () {
-    var self = this;
-
-    var createSocketFunc = exports.createSocket || function() {
-      var protocol = 'ws:';
-      if (window.location.protocol === 'https:')
-        protocol = 'wss:';
-
-      var defaultPath = window.location.pathname;
-      // some older WebKit browsers return the pathname already decoded;
-      // if we find invalid URL characters in the path, encode them
-      if (!/^([$#!&-;=?-[\]_a-z~]|%[0-9a-fA-F]{2})+$/.test(defaultPath)) {
-        defaultPath = encodeURI(defaultPath);
-        // Bizarrely, QtWebKit requires us to encode these characters *twice*
-        if (browser.isQt) {
+        var defaultPath = window.location.pathname;
+        // some older WebKit browsers return the pathname already decoded;
+        // if we find invalid URL characters in the path, encode them
+        if (!/^([$#!&-;=?-[\]_a-z~]|%[0-9a-fA-F]{2})+$/.test(defaultPath)) {
           defaultPath = encodeURI(defaultPath);
+          // Bizarrely, QtWebKit requires us to encode these characters *twice*
+          if (browser.isQt) {
+            defaultPath = encodeURI(defaultPath);
+          }
         }
-      }
-      if (!/\/$/.test(defaultPath))
-        defaultPath += '/';
-      defaultPath += 'websocket/';
+        if (!/\/$/.test(defaultPath)) defaultPath += '/';
+        defaultPath += 'websocket/';
 
-      var ws = new WebSocket(protocol + '//' + window.location.host + defaultPath);
-      ws.binaryType = 'arraybuffer';
-      return ws;
-    };
+        var ws = new WebSocket(protocol + '//' + window.location.host + defaultPath);
+        ws.binaryType = 'arraybuffer';
 
-    var socket = createSocketFunc();
-    socket.onopen = function() {
-      $(document).trigger({
-        type: 'shiny:connected',
-        socket: socket
-      });
-      socket.send(JSON.stringify({
-        method: 'init',
-        data: self.$initialInput
-      }));
+        return ws;
+      };
 
-      while (self.$pendingMessages.length) {
-        var msg = self.$pendingMessages.shift();
-        socket.send(msg);
-      }
-    };
-    socket.onmessage = function(e) {
-      self.dispatchMessage(e.data);
-    };
-    socket.onclose = function() {
-      $(document).trigger({
-        type: 'shiny:disconnected',
-        socket: socket
-      });
-      $(document.body).addClass('disconnected');
-      self.$notifyDisconnected();
-    };
-    return socket;
-  };
+      var socket = createSocketFunc();
+      var hasOpened = false;
+      socket.onopen = function () {
+        hasOpened = true;
 
-  this.sendInput = function(values) {
-    var msg = JSON.stringify({
-      method: 'update',
-      data: values
-    });
+        $(document).trigger({
+          type: 'shiny:connected',
+          socket: socket
+        });
 
-    this.$sendMsg(msg);
+        self.onConnected();
 
-    $.extend(this.$inputValues, values);
-    this.$updateConditionals();
-  };
+        socket.send(JSON.stringify({
+          method: 'init',
+          data: self.$initialInput
+        }));
+
+        while (self.$pendingMessages.length) {
+          var msg = self.$pendingMessages.shift();
+          socket.send(msg);
+        }
+      };
+      socket.onmessage = function (e) {
+        self.dispatchMessage(e.data);
+      };
+      // Called when a successfully-opened websocket is closed, or when an
+      // attempt to open a connection fails.
+      socket.onclose = function () {
+        // These things are needed only if we've successfully opened the
+        // websocket.
+        if (hasOpened) {
+          $(document).trigger({
+            type: 'shiny:disconnected',
+            socket: socket
+          });
 
-  this.$notifyDisconnected = function() {
+          self.$notifyDisconnected();
+        }
 
-    // function to normalize hostnames
-    var normalize = function(hostname) {
-      if (hostname == "127.0.0.1")
-        return "localhost";
-      else
-        return hostname;
+        self.onDisconnected(); // Must be run before self.$removeSocket()
+        self.$removeSocket();
+      };
+      return socket;
     };
 
-    // Send a 'disconnected' message to parent if we are on the same domin
-    var parentUrl = (parent !== window) ? document.referrer : null;
-    if (parentUrl) {
-      // parse the parent href
-      var a = document.createElement('a');
-      a.href = parentUrl;
-
-      // post the disconnected message if the hostnames are the same
-      if (normalize(a.hostname) == normalize(window.location.hostname)) {
-        var protocol = a.protocol.replace(':',''); // browser compatability
-        var origin = protocol + '://' + a.hostname;
-        if (a.port)
-          origin = origin + ':' + a.port;
-        parent.postMessage('disconnected', origin);
-      }
-    }
-  };
+    this.sendInput = function (values) {
+      var msg = JSON.stringify({
+        method: 'update',
+        data: values
+      });
 
-  // 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 a string describing the error.
-  // @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.$sendMsg(msg);
 
-    this.$activeRequests[requestId] = {
-      onSuccess: onSuccess,
-      onError: onError
+      $.extend(this.$inputValues, values);
+      this.$updateConditionals();
     };
 
-    var msg = JSON.stringify({
-      method: method,
-      args: args,
-      tag: requestId
-    });
+    this.$notifyDisconnected = function () {
 
-    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.
-
-      var uint32_to_buf = function(val) {
-        var buffer = new ArrayBuffer(4);
-        var view = new DataView(buffer);
-        view.setUint32(0, val, true); // little-endian
-        return buffer;
+      // function to normalize hostnames
+      var normalize = function normalize(hostname) {
+        if (hostname === "127.0.0.1") return "localhost";else return hostname;
       };
 
-      var payload = [];
-      payload.push(uint32_to_buf(0x01020202)); // signature
+      // Send a 'disconnected' message to parent if we are on the same domin
+      var parentUrl = parent !== window ? document.referrer : null;
+      if (parentUrl) {
+        // parse the parent href
+        var a = document.createElement('a');
+        a.href = parentUrl;
+
+        // post the disconnected message if the hostnames are the same
+        if (normalize(a.hostname) === normalize(window.location.hostname)) {
+          var protocol = a.protocol.replace(':', ''); // browser compatability
+          var origin = protocol + '://' + a.hostname;
+          if (a.port) origin = origin + ':' + a.port;
+          parent.postMessage('disconnected', origin);
+        }
+      }
+    };
 
-      var jsonBuf = makeBlob([msg]);
-      payload.push(uint32_to_buf(jsonBuf.size));
-      payload.push(jsonBuf);
+    this.$removeSocket = function () {
+      this.$socket = null;
+    };
 
-      for (var i = 0; i < blobs.length; i++) {
-        payload.push(uint32_to_buf(blobs[i].byteLength || blobs[i].size || 0));
-        payload.push(blobs[i]);
-      }
+    this.$scheduleReconnect = function (delay) {
+      var self = this;
+      scheduledReconnect = setTimeout(function () {
+        self.reconnect();
+      }, delay);
+    };
 
-      msg = makeBlob(payload);
-    }
+    // How long should we wait before trying the next reconnection?
+    // The delay will increase with subsequent attempts.
+    // .next: Return the time to wait for next connection, and increment counter.
+    // .reset: Reset the attempt counter.
+    var reconnectDelay = function () {
+      var attempts = 0;
+      // Time to wait before each reconnection attempt. If we go through all of
+      // these values, repeated use the last one. Add 500ms to each one so that
+      // in the last 0.5s, it shows "..."
+      var delays = [1500, 1500, 2500, 2500, 5500, 5500, 10500];
 
-    this.$sendMsg(msg);
-  };
+      return {
+        next: function next() {
+          var i = attempts;
+          // Instead of going off the end, use the last one
+          if (i >= delays.length) {
+            i = delays.length - 1;
+          }
 
-  this.$sendMsg = function(msg) {
-    if (!this.$socket.readyState) {
-      this.$pendingMessages.push(msg);
-    }
-    else {
-      this.$socket.send(msg);
-    }
-  };
+          attempts++;
+          return delays[i];
+        },
+        reset: function reset() {
+          attempts = 0;
+        }
+      };
+    }();
 
-  this.receiveError = function(name, error) {
-    if (this.$errors[name] === error)
-      return;
+    this.onDisconnected = function () {
+      // Add gray-out overlay, if not already present
+      var $overlay = $('#shiny-disconnected-overlay');
+      if ($overlay.length === 0) {
+        $(document.body).append('<div id="shiny-disconnected-overlay"></div>');
+      }
 
-    this.$errors[name] = error;
-    delete this.$values[name];
-
-    var binding = this.$bindings[name];
-    var evt = jQuery.Event('shiny:error');
-    evt.name = name;
-    evt.error = error;
-    evt.binding = binding;
-    $(binding ? binding.el : document).trigger(evt);
-    if (!evt.isDefaultPrevented() && binding && binding.onValueError) {
-      binding.onValueError(evt.error);
-    }
-  };
+      // To try a reconnect, both the app (this.$allowReconnect) and the
+      // server (this.$socket.allowReconnect) must allow reconnections, or
+      // session$allowReconnect("force") was called. The "force" option should
+      // only be used for testing.
+      if (this.$allowReconnect === true && this.$socket.allowReconnect === true || this.$allowReconnect === "force") {
+        var delay = reconnectDelay.next();
+        exports.showReconnectDialog(delay);
+        this.$scheduleReconnect(delay);
+      }
+    };
 
-  this.receiveOutput = function(name, value) {
-    if (this.$values[name] === value)
-      return;
+    this.onConnected = function () {
+      $('#shiny-disconnected-overlay').remove();
+      exports.hideReconnectDialog();
+      reconnectDelay.reset();
+    };
 
-    this.$values[name] = value;
-    delete this.$errors[name];
-
-    var binding = this.$bindings[name];
-    var evt = jQuery.Event('shiny:value');
-    evt.name = name;
-    evt.value = value;
-    evt.binding = binding;
-    $(binding ? binding.el : document).trigger(evt);
-    if (!evt.isDefaultPrevented() && binding) {
-      binding.onValueChange(evt.value);
-    }
+    // 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 a string describing the error.
+    // @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;
 
-    return value;
-  };
+      this.$activeRequests[requestId] = {
+        onSuccess: onSuccess,
+        onError: onError
+      };
 
-  this.bindOutput = function(id, binding) {
-    if (!id)
-      throw "Can't bind an element with no ID";
-    if (this.$bindings[id])
-      throw "Duplicate binding for ID " + id;
-    this.$bindings[id] = binding;
+      var msg = JSON.stringify({
+        method: method,
+        args: args,
+        tag: requestId
+      });
 
-    if (this.$values[id] !== undefined)
-      binding.onValueChange(this.$values[id]);
-    else if (this.$errors[id] !== undefined)
-      binding.onValueError(this.$errors[id]);
+      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.
+
+        var uint32_to_buf = function uint32_to_buf(val) {
+          var buffer = new ArrayBuffer(4);
+          var view = new DataView(buffer);
+          view.setUint32(0, val, true); // little-endian
+          return buffer;
+        };
 
-    return binding;
-  };
+        var payload = [];
+        payload.push(uint32_to_buf(0x01020202)); // signature
 
-  this.unbindOutput = function(id, binding) {
-    if (this.$bindings[id] === binding) {
-      delete this.$bindings[id];
-      return true;
-    }
-    else {
-      return false;
-    }
-  };
+        var jsonBuf = makeBlob([msg]);
+        payload.push(uint32_to_buf(jsonBuf.size));
+        payload.push(jsonBuf);
 
-  this.$updateConditionals = function() {
-    $(document).trigger({
-      type: 'shiny:conditional'
-    });
+        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);
+      }
 
-    var inputs = {};
+      this.$sendMsg(msg);
+    };
 
-    // Input keys use "name:type" format; we don't want the user to
-    // have to know about the type suffix when referring to inputs.
-    for (var name in this.$inputValues) {
-      if (this.$inputValues.hasOwnProperty(name)) {
-        var shortName = name.replace(/:.*/, '');
-        inputs[shortName] = this.$inputValues[name];
+    this.$sendMsg = function (msg) {
+      if (!this.$socket.readyState) {
+        this.$pendingMessages.push(msg);
+      } else {
+        this.$socket.send(msg);
       }
-    }
+    };
 
-    var scope = {input: inputs, output: this.$values};
+    this.receiveError = function (name, error) {
+      if (this.$errors[name] === error) return;
 
-    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');
+      this.$errors[name] = error;
+      delete this.$values[name];
 
-      if (!condFunc) {
-        var condExpr = el.attr('data-display-if');
-        condFunc = scopeExprToFunc(condExpr);
-        el.data('data-display-if-func', condFunc);
+      var binding = this.$bindings[name];
+      var evt = jQuery.Event('shiny:error');
+      evt.name = name;
+      evt.error = error;
+      evt.binding = binding;
+      $(binding ? binding.el : document).trigger(evt);
+      if (!evt.isDefaultPrevented() && binding && binding.onValueError) {
+        binding.onValueError(evt.error);
       }
+    };
 
-      var show = condFunc(scope);
-      var showing = el.css("display") !== "none";
-      if (show !== showing) {
-        if (show) {
-          el.trigger('show');
-          el.show();
-          el.trigger('shown');
-        }
-        else {
-          el.trigger('hide');
-          el.hide();
-          el.trigger('hidden');
-        }
+    this.receiveOutput = function (name, value) {
+      if (this.$values[name] === value) return undefined;
+
+      this.$values[name] = value;
+      delete this.$errors[name];
+
+      var binding = this.$bindings[name];
+      var evt = jQuery.Event('shiny:value');
+      evt.name = name;
+      evt.value = value;
+      evt.binding = binding;
+      $(binding ? binding.el : document).trigger(evt);
+      if (!evt.isDefaultPrevented() && binding) {
+        binding.onValueChange(evt.value);
       }
-    }
-  };
 
-  // Message handler management functions =================================
-
-  // Records insertion order of handlers. Maps number to name. This is so
-  // we can dispatch messages to handlers in the order that handlers were
-  // added.
-  var messageHandlerOrder = [];
-  // Keep track of handlers by name. Maps name to handler function.
-  var messageHandlers = {};
-
-  // Two categories of message handlers: those that are from Shiny, and those
-  // that are added by the user. The Shiny ones handle messages in
-  // msgObj.values, msgObj.errors, and so on. The user ones handle messages
-  // in msgObj.custom.foo and msgObj.custom.bar.
-  var customMessageHandlerOrder = [];
-  var customMessageHandlers = {};
-
-  // Adds Shiny (internal) message handler
-  function addMessageHandler(type, handler) {
-    if (messageHandlers[type]) {
-      throw('handler for message of type "' + type + '" already added.');
-    }
-    if (typeof(handler) !== 'function') {
-      throw('handler must be a function.');
-    }
-    if (handler.length !== 1) {
-      throw('handler must be a function that takes one argument.');
-    }
+      return value;
+    };
 
-    messageHandlerOrder.push(type);
-    messageHandlers[type] = handler;
-  }
+    this.bindOutput = function (id, binding) {
+      if (!id) throw "Can't bind an element with no ID";
+      if (this.$bindings[id]) throw "Duplicate binding for ID " + id;
+      this.$bindings[id] = binding;
 
-  // Adds custom message handler - this one is exposed to the user
-  function addCustomMessageHandler(type, handler) {
-    if (customMessageHandlers[type]) {
-      throw('handler for message of type "' + type + '" already added.');
-    }
-    if (typeof(handler) !== 'function') {
-      throw('handler must be a function.');
-    }
-    if (handler.length !== 1) {
-      throw('handler must be a function that takes one argument.');
-    }
+      if (this.$values[id] !== undefined) binding.onValueChange(this.$values[id]);else if (this.$errors[id] !== undefined) binding.onValueError(this.$errors[id]);
 
-    customMessageHandlerOrder.push(type);
-    customMessageHandlers[type] = handler;
-  }
+      return binding;
+    };
 
-  exports.addCustomMessageHandler = addCustomMessageHandler;
+    this.unbindOutput = function (id, binding) {
+      if (this.$bindings[id] === binding) {
+        delete this.$bindings[id];
+        return true;
+      } else {
+        return false;
+      }
+    };
 
-  this.dispatchMessage = function(msg) {
-    var msgObj = JSON.parse(msg);
+    this.$updateConditionals = function () {
+      $(document).trigger({
+        type: 'shiny:conditional'
+      });
 
-    var evt = jQuery.Event('shiny:message');
-    evt.message = msgObj;
-    $(document).trigger(evt);
-    if (evt.isDefaultPrevented()) return;
+      var inputs = {};
 
-    // Send msgObj.foo and msgObj.bar to appropriate handlers
-    this._sendMessagesToHandlers(evt.message, messageHandlers, messageHandlerOrder);
+      // Input keys use "name:type" format; we don't want the user to
+      // have to know about the type suffix when referring to inputs.
+      for (var name in this.$inputValues) {
+        if (this.$inputValues.hasOwnProperty(name)) {
+          var shortName = name.replace(/:.*/, '');
+          inputs[shortName] = this.$inputValues[name];
+        }
+      }
 
-    this.$updateConditionals();
-  };
+      var scope = { input: inputs, 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');
 
-  // A function for sending messages to the appropriate handlers.
-  // - msgObj: the object containing messages, with format {msgObj.foo, msObj.bar
-  this._sendMessagesToHandlers = function(msgObj, handlers, handlerOrder) {
-    // Dispatch messages to handlers, if handler is present
-    for (var i = 0; i < handlerOrder.length; i++) {
-      var msgType = handlerOrder[i];
+        if (!condFunc) {
+          var condExpr = el.attr('data-display-if');
+          condFunc = scopeExprToFunc(condExpr);
+          el.data('data-display-if-func', condFunc);
+        }
 
-      if (msgObj.hasOwnProperty(msgType)) {
-        // Execute each handler with 'this' referring to the present value of
-        // 'this'
-        handlers[msgType].call(this, msgObj[msgType]);
+        var show = condFunc(scope);
+        var showing = el.css("display") !== "none";
+        if (show !== showing) {
+          if (show) {
+            el.trigger('show');
+            el.show();
+            el.trigger('shown');
+          } else {
+            el.trigger('hide');
+            el.hide();
+            el.trigger('hidden');
+          }
+        }
       }
-    }
-  };
-
-  // Message handlers =====================================================
+    };
 
-  addMessageHandler('values', function(message) {
-    for (var name in this.$bindings) {
-      if (this.$bindings.hasOwnProperty(name))
-        this.$bindings[name].showProgress(false);
-    }
+    // Message handler management functions =================================
+
+    // Records insertion order of handlers. Maps number to name. This is so
+    // we can dispatch messages to handlers in the order that handlers were
+    // added.
+    var messageHandlerOrder = [];
+    // Keep track of handlers by name. Maps name to handler function.
+    var messageHandlers = {};
+
+    // Two categories of message handlers: those that are from Shiny, and those
+    // that are added by the user. The Shiny ones handle messages in
+    // msgObj.values, msgObj.errors, and so on. The user ones handle messages
+    // in msgObj.custom.foo and msgObj.custom.bar.
+    var customMessageHandlerOrder = [];
+    var customMessageHandlers = {};
+
+    // Adds Shiny (internal) message handler
+    function addMessageHandler(type, handler) {
+      if (messageHandlers[type]) {
+        throw 'handler for message of type "' + type + '" already added.';
+      }
+      if (typeof handler !== 'function') {
+        throw 'handler must be a function.';
+      }
+      if (handler.length !== 1) {
+        throw 'handler must be a function that takes one argument.';
+      }
+      messageHandlerOrder.push(type);
+      messageHandlers[type] = handler;
+    }
+
+    // Adds custom message handler - this one is exposed to the user
+    function addCustomMessageHandler(type, handler) {
+      // Remove any previously defined handlers so that only the most recent one
+      // will be called
+      if (customMessageHandlers[type]) {
+        var typeIdx = customMessageHandlerOrder.indexOf(type);
+        if (typeIdx !== -1) {
+          customMessageHandlerOrder.splice(typeIdx, 1);
+          delete customMessageHandlers[type];
+        }
+      }
+      if (typeof handler !== 'function') {
+        throw 'handler must be a function.';
+      }
+      if (handler.length !== 1) {
+        throw 'handler must be a function that takes one argument.';
+      }
 
-    for (var key in message) {
-      if (message.hasOwnProperty(key))
-        this.receiveOutput(key, message[key]);
+      customMessageHandlerOrder.push(type);
+      customMessageHandlers[type] = handler;
     }
-  });
 
-  addMessageHandler('errors', function(message) {
-    for (var key in message) {
-      if (message.hasOwnProperty(key))
-        this.receiveError(key, message[key]);
-    }
-  });
+    exports.addCustomMessageHandler = addCustomMessageHandler;
 
-  addMessageHandler('inputMessages', function(message) {
-    // inputMessages should be an array
-    for (var i = 0; i < message.length; i++) {
-      var $obj = $('.shiny-bound-input#' + $escape(message[i].id));
-      var inputBinding = $obj.data('shiny-input-binding');
-
-      // Dispatch the message to the appropriate input object
-      if ($obj.length > 0) {
-        var el = $obj[0];
-        var evt = jQuery.Event('shiny:updateinput');
-        evt.message = message[i].message;
-        evt.binding = inputBinding;
-        $(el).trigger(evt);
-        if (!evt.isDefaultPrevented())
-          inputBinding.receiveMessage(el, evt.message);
+    this.dispatchMessage = function (data) {
+      var msgObj = {};
+      if (typeof data === "string") {
+        msgObj = JSON.parse(data);
+      } else {
+        // data is arraybuffer
+        var len = new DataView(data, 0, 1).getUint8(0);
+        var typedv = new DataView(data, 1, len);
+        var typebuf = [];
+        for (var i = 0; i < len; i++) {
+          typebuf.push(String.fromCharCode(typedv.getUint8(i)));
+        }
+        var type = typebuf.join("");
+        data = data.slice(len + 1);
+        msgObj.custom = {};
+        msgObj.custom[type] = data;
       }
-    }
-  });
 
-  addMessageHandler('javascript', function(message) {
-    /*jshint evil: true */
-    eval(message);
-  });
+      var evt = jQuery.Event('shiny:message');
+      evt.message = msgObj;
+      $(document).trigger(evt);
+      if (evt.isDefaultPrevented()) return;
 
-  addMessageHandler('console', function(message) {
-    for (var i = 0; i < message.length; i++) {
-      if (console.log)
-        console.log(message[i]);
-    }
-  });
+      // Send msgObj.foo and msgObj.bar to appropriate handlers
+      this._sendMessagesToHandlers(evt.message, messageHandlers, messageHandlerOrder);
 
-  addMessageHandler('progress', function(message) {
-    if (message.type && message.message) {
-      var handler = progressHandlers[message.type];
-      if (handler)
-        handler.call(this, message.message);
-    }
-  });
+      this.$updateConditionals();
+    };
 
-  addMessageHandler('response', function(message) {
-    var requestId = message.tag;
-    var request = this.$activeRequests[requestId];
-    if (request) {
-      delete this.$activeRequests[requestId];
-      if ('value' in message)
-        request.onSuccess(message.value);
-      else
-        request.onError(message.error);
-    }
-  });
+    // A function for sending messages to the appropriate handlers.
+    // - msgObj: the object containing messages, with format {msgObj.foo, msObj.bar
+    this._sendMessagesToHandlers = function (msgObj, handlers, handlerOrder) {
+      // Dispatch messages to handlers, if handler is present
+      for (var i = 0; i < handlerOrder.length; i++) {
+        var msgType = handlerOrder[i];
+        if (msgObj.hasOwnProperty(msgType)) {
+          // Execute each handler with 'this' referring to the present value of
+          // 'this'
+          handlers[msgType].call(this, msgObj[msgType]);
+        }
+      }
+    };
 
-  addMessageHandler('custom', function(message) {
-    // For old-style custom messages - should deprecate and migrate to new
-    // method
-    if (exports.oncustommessage) {
-      exports.oncustommessage(message);
-    }
+    // Message handlers =====================================================
 
-    // Send messages.foo and messages.bar to appropriate handlers
-    this._sendMessagesToHandlers(message, customMessageHandlers,
-                                 customMessageHandlerOrder);
-  });
+    addMessageHandler('values', function (message) {
+      for (var name in this.$bindings) {
+        if (this.$bindings.hasOwnProperty(name)) this.$bindings[name].showProgress(false);
+      }
 
-  addMessageHandler('config', function(message) {
-    this.config = message;
-  });
+      for (var key in message) {
+        if (message.hasOwnProperty(key)) this.receiveOutput(key, message[key]);
+      }
+    });
+
+    addMessageHandler('errors', function (message) {
+      for (var key in message) {
+        if (message.hasOwnProperty(key)) this.receiveError(key, message[key]);
+      }
+    });
 
+    addMessageHandler('inputMessages', function (message) {
+      // inputMessages should be an array
+      for (var i = 0; i < message.length; i++) {
+        var $obj = $('.shiny-bound-input#' + $escape(message[i].id));
+        var inputBinding = $obj.data('shiny-input-binding');
+
+        // Dispatch the message to the appropriate input object
+        if ($obj.length > 0) {
+          var el = $obj[0];
+          var evt = jQuery.Event('shiny:updateinput');
+          evt.message = message[i].message;
+          evt.binding = inputBinding;
+          $(el).trigger(evt);
+          if (!evt.isDefaultPrevented()) inputBinding.receiveMessage(el, evt.message);
+        }
+      }
+    });
 
-  addCustomMessageHandler('busy', function(message) {
-    if (message === 'busy') {
-      $(document.documentElement).addClass('shiny-busy');
-      $(document).trigger('shiny:busy');
-    } else if (message === 'idle') {
-      $(document.documentElement).removeClass('shiny-busy');
-      $(document).trigger('shiny:idle');
-    }
-  });
+    addMessageHandler('javascript', function (message) {
+      /*jshint evil: true */
+      eval(message);
+    });
 
-  addCustomMessageHandler('recalculating', function(message) {
-    if (message.hasOwnProperty('name') && message.hasOwnProperty('status')) {
-      var binding = this.$bindings[message.name];
-      $(binding ? binding.el : null).trigger({
-        type: 'shiny:' + message.status
-      });
-    }
-  });
+    addMessageHandler('console', function (message) {
+      for (var i = 0; i < message.length; i++) {
+        if (console.log) console.log(message[i]);
+      }
+    });
 
-  addCustomMessageHandler('reload', function(message) {
-    window.location.reload();
-  });
+    addMessageHandler('progress', function (message) {
+      if (message.type && message.message) {
+        var handler = progressHandlers[message.type];
+        if (handler) handler.call(this, message.message);
+      }
+    });
 
+    addMessageHandler('notification', function (message) {
+      if (message.type === 'show') exports.notifications.show(message.message);else if (message.type === 'remove') exports.notifications.remove(message.message);else throw 'Unkown notification type: ' + message.type;
+    });
 
-  // Progress reporting ====================================================
+    addMessageHandler('modal', function (message) {
+      if (message.type === 'show') exports.modal.show(message.message);else if (message.type === 'remove') exports.modal.remove(); // For 'remove', message content isn't used
+      else throw 'Unkown modal type: ' + message.type;
+    });
 
-  var progressHandlers = {
-    // Progress for a particular object
-    binding: function(message) {
-      var key = message.id;
-      var binding = this.$bindings[key];
-      if (binding && binding.showProgress) {
-        binding.showProgress(true);
+    addMessageHandler('response', function (message) {
+      var requestId = message.tag;
+      var request = this.$activeRequests[requestId];
+      if (request) {
+        delete this.$activeRequests[requestId];
+        if ('value' in message) request.onSuccess(message.value);else request.onError(message.error);
       }
-    },
-    // Open a page-level progress bar
-    open: function(message) {
-      // Add progress container (for all progress items) if not already present
-      var $container = $('.shiny-progress-container');
-      if ($container.length === 0) {
-        $container = $('<div class="shiny-progress-container"></div>');
-        $('body').append($container);
-      }
-
-      // Add div for just this progress ID
-      var depth = $('.shiny-progress.open').length;
-      var $progress = $(progressHandlers.progressHTML);
-      $progress.attr('id', message.id);
-      $container.append($progress);
-
-      // Stack bars
-      var $progressBar = $progress.find('.progress');
-      $progressBar.css('top', depth * $progressBar.height() + 'px');
-
-      // Stack text objects
-      var $progressText = $progress.find('.progress-text');
-      $progressText.css('top', 3 * $progressBar.height() +
-        depth * $progressText.outerHeight() + 'px');
-
-      $progress.hide();
-    },
+    });
 
-    // Update page-level progress bar
-    update: function(message) {
-      var $progress = $('#' + message.id + '.shiny-progress');
-      if (typeof(message.message) !== 'undefined') {
-        $progress.find('.progress-message').text(message.message);
+    addMessageHandler('allowReconnect', function (message) {
+      if (message === true || message === false || message === "force") {
+        this.$allowReconnect = message;
+      } else {
+        throw "Invalid value for allowReconnect: " + message;
       }
-      if (typeof(message.detail) !== 'undefined') {
-        $progress.find('.progress-detail').text(message.detail);
+    });
+
+    addMessageHandler('custom', function (message) {
+      // For old-style custom messages - should deprecate and migrate to new
+      // method
+      if (exports.oncustommessage) {
+        exports.oncustommessage(message);
       }
-      if (typeof(message.value) !== 'undefined') {
-        if (message.value !== null) {
-          $progress.find('.progress').show();
-          $progress.find('.bar').width((message.value*100) + '%');
-        }
-        else {
-          $progress.find('.progress').hide();
-        }
+
+      // Send messages.foo and messages.bar to appropriate handlers
+      this._sendMessagesToHandlers(message, customMessageHandlers, customMessageHandlerOrder);
+    });
+
+    addMessageHandler('config', function (message) {
+      this.config = message;
+    });
+
+    addMessageHandler('busy', function (message) {
+      if (message === 'busy') {
+        $(document.documentElement).addClass('shiny-busy');
+        $(document).trigger('shiny:busy');
+      } else if (message === 'idle') {
+        $(document.documentElement).removeClass('shiny-busy');
+        $(document).trigger('shiny:idle');
       }
+    });
 
-      $progress.fadeIn();
-    },
+    addMessageHandler('recalculating', function (message) {
+      if (message.hasOwnProperty('name') && message.hasOwnProperty('status')) {
+        var binding = this.$bindings[message.name];
+        $(binding ? binding.el : null).trigger({
+          type: 'shiny:' + message.status
+        });
+      }
+    });
 
-    // Close page-level progress bar
-    close: function(message) {
-      var $progress = $('#' + message.id + '.shiny-progress');
-      $progress.removeClass('open');
+    addMessageHandler('reload', function (message) {
+      window.location.reload();
+    });
 
-      $progress.fadeOut({
-        complete: function() {
-          $progress.remove();
+    addMessageHandler('shiny-insert-ui', function (message) {
+      var targets = $(message.selector);
+      if (targets.length === 0) {
+        // render the HTML and deps to a null target, so
+        // the side-effect of rendering the deps, singletons,
+        // and <head> still occur
+        exports.renderHtml($([]), message.content.html, message.content.deps);
+      } else {
+        targets.each(function (i, target) {
+          exports.renderContent(target, message.content, message.where);
+          return message.multiple;
+        });
+      }
+    });
 
-          // If this was the last shiny-progress, remove container
-          if ($('.shiny-progress').length === 0)
-            $('.shiny-progress-container').remove();
-        }
+    addMessageHandler('shiny-remove-ui', function (message) {
+      var els = $(message.selector);
+      els.each(function (i, el) {
+        exports.unbindAll(el, true);
+        $(el).remove();
+        // If `multiple` is false, returning false terminates the function
+        // and no other elements are removed; if `multiple` is true,
+        // returning true continues removing all remaining elements.
+        return message.multiple;
       });
-    },
-
-    // The 'bar' class is needed for backward compatibility with Bootstrap 2.
-    progressHTML: '<div class="shiny-progress open">' +
-      '<div class="progress progress-striped active"><div class="progress-bar bar"></div></div>' +
-      '<div class="progress-text">' +
-        '<span class="progress-message">message</span>' +
-        '<span class="progress-detail"></span>' +
-      '</div>' +
-    '</div>'
-  };
+    });
 
-  exports.progressHandlers = progressHandlers;
+    addMessageHandler('updateQueryString', function (message) {
+      window.history.replaceState(null, null, message.queryString);
+    });
 
+    addMessageHandler("resetBrush", function (message) {
+      exports.resetBrush(message.brushId);
+    });
 
-}).call(ShinyApp.prototype);
+    // Progress reporting ====================================================
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/file_processor.js
-
-// 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.fileIndex = -1;
-  // Currently need to use small chunk size because R-Websockets can't
-  // handle continuation frames
-  this.aborted = false;
-  this.completed = false;
-
-  // 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.onFile = function(file, cont) {
-    setTimeout(cont, 0);
-  };
-  this.onComplete = function() {
-  };
-  this.onAbort = function() {
-  };
-  // End callbacks
+    var progressHandlers = {
+      // Progress for a particular object
+      binding: function binding(message) {
+        var key = message.id;
+        var binding = this.$bindings[key];
+        if (binding && binding.showProgress) {
+          binding.showProgress(true);
+        }
+      },
 
-  // Aborts processing, unless it's already completed
-  this.abort = function() {
-    if (this.completed || this.aborted)
-      return;
+      // Open a page-level progress bar
+      open: function open(message) {
+        if (message.style === "notification") {
+          // For new-style (starting in Shiny 0.14) progress indicators that use
+          // the notification API.
+
+          // Progress bar starts hidden; will be made visible if a value is provided
+          // during updates.
+          exports.notifications.show({
+            html: '<div id="shiny-progress-' + message.id + '" class="shiny-progress-notification">' + '<div class="progress progress-striped active" style="display: none;"><div class="progress-bar"></div></div>' + '<div class="progress-text">' + '<span class="progress-message">message</span> ' + '<span class="progress-detail"></span>' + '</div>' + '</div>',
+            id: message.id,
+            duration: null
+          });
+        } else if (message.style === "old") {
+          // For old-style (Shiny <=0.13.2) progress indicators.
+
+          // Add progress container (for all progress items) if not already present
+          var $container = $('.shiny-progress-container');
+          if ($container.length === 0) {
+            $container = $('<div class="shiny-progress-container"></div>');
+            $('body').append($container);
+          }
 
-    this.aborted = true;
-    this.onAbort();
-  };
+          // Add div for just this progress ID
+          var depth = $('.shiny-progress.open').length;
+          // The 'bar' class is needed for backward compatibility with Bootstrap 2.
+          var $progress = $('<div class="shiny-progress open">' + '<div class="progress progress-striped active"><div class="progress-bar bar"></div></div>' + '<div class="progress-text">' + '<span class="progress-message">message</span>' + '<span class="progress-detail"></span>' + '</div>' + '</div>');
 
-  // 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();
-    };
-  };
+          $progress.attr('id', message.id);
+          $container.append($progress);
 
-  // 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() {
+          // Stack bars
+          var $progressBar = $progress.find('.progress');
+          $progressBar.css('top', depth * $progressBar.height() + 'px');
 
-    var self = this;
+          // Stack text objects
+          var $progressText = $progress.find('.progress-text');
+          $progressText.css('top', 3 * $progressBar.height() + depth * $progressText.outerHeight() + 'px');
 
-    if (this.aborted || this.completed)
-      return;
+          $progress.hide();
+        }
+      },
 
-    if (this.fileIndex < 0) {
-      // Haven't started yet--begin
-      this.fileIndex = 0;
-      this.onBegin(this.files, this.$getRun());
-      return;
-    }
+      // Update page-level progress bar
+      update: function update(message) {
+        if (message.style === "notification") {
+          // For new-style (starting in Shiny 0.14) progress indicators that use
+          // the notification API.
+          var $progress = $('#shiny-progress-' + message.id);
 
-    if (this.fileIndex === this.files.length) {
-      // Just ended
-      this.completed = true;
-      this.onComplete();
-      return;
-    }
+          if ($progress.length === 0) 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.
+          if (typeof message.message !== 'undefined') {
+            $progress.find('.progress-message').text(message.message);
+          }
+          if (typeof message.detail !== 'undefined') {
+            $progress.find('.progress-detail').text(message.detail);
+          }
+          if (typeof message.value !== 'undefined') {
+            if (message.value !== null) {
+              $progress.find('.progress').show();
+              $progress.find('.progress-bar').width(message.value * 100 + '%');
+            } else {
+              $progress.find('.progress').hide();
+            }
+          }
+        } else if (message.style === "old") {
+          // For old-style (Shiny <=0.13.2) progress indicators.
 
-    var file = this.files[this.fileIndex++];
-    this.onFile(file, this.$getRun());
-  };
-}).call(FileProcessor.prototype);
+          var $progress = $('#' + message.id + '.shiny-progress');
+          if (typeof message.message !== 'undefined') {
+            $progress.find('.progress-message').text(message.message);
+          }
+          if (typeof message.detail !== 'undefined') {
+            $progress.find('.progress-detail').text(message.detail);
+          }
+          if (typeof message.value !== 'undefined') {
+            if (message.value !== null) {
+              $progress.find('.progress').show();
+              $progress.find('.bar').width(message.value * 100 + '%');
+            } else {
+              $progress.find('.progress').hide();
+            }
+          }
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/binding_registry.js
-
-var BindingRegistry = function() {
-  this.bindings = [];
-  this.bindingNames = {};
-};
-(function() {
-  this.register = function(binding, bindingName, priority) {
-    var bindingObj = {binding: binding, priority: priority || 0};
-    this.bindings.unshift(bindingObj);
-    if (bindingName) {
-      this.bindingNames[bindingName] = bindingObj;
-      binding.name = bindingName;
-    }
-  };
-  this.setPriority = function(bindingName, priority) {
-    var bindingObj = this.bindingNames[bindingName];
-    if (!bindingObj)
-      throw "Tried to set priority on unknown binding " + bindingName;
-    bindingObj.priority = priority || 0;
-  };
-  this.getPriority = function(bindingName) {
-    var bindingObj = this.bindingNames[bindingName];
-    if (!bindingObj)
-      return false;
-    return bindingObj.priority;
-  };
-  this.getBindings = function() {
-    // Sort the bindings. The ones with higher priority are consulted
-    // first; ties are broken by most-recently-registered.
-    return mergeSort(this.bindings, function(a, b) {
-      return b.priority - a.priority;
-    });
-  };
-}).call(BindingRegistry.prototype);
+          $progress.fadeIn();
+        }
+      },
 
+      // Close page-level progress bar
+      close: function close(message) {
+        if (message.style === "notification") {
+          exports.notifications.remove(message.id);
+        } else if (message.style === "old") {
+          var $progress = $('#' + message.id + '.shiny-progress');
+          $progress.removeClass('open');
 
-var inputBindings = exports.inputBindings = new BindingRegistry();
-var outputBindings = exports.outputBindings = new BindingRegistry();
+          $progress.fadeOut({
+            complete: function complete() {
+              $progress.remove();
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/output_binding.js
+              // If this was the last shiny-progress, remove container
+              if ($('.shiny-progress').length === 0) $('.shiny-progress-container').remove();
+            }
+          });
+        }
+      }
+    };
 
-var OutputBinding = exports.OutputBinding = function() {};
-(function() {
-  // Returns a jQuery object or element array that contains the
-  // descendants of scope that match this binding
-  this.find = function(scope) { throw "Not implemented"; };
+    exports.progressHandlers = progressHandlers;
 
-  this.getId = function(el) {
-    return el['data-input-id'] || el.id;
-  };
+    // Returns a URL which can be queried to get values from inside the server
+    // function. This is enabled with `options(shiny.testmode=TRUE)`.
+    this.getTestEndpointUrl = function () {
+      return "session/" + encodeURIComponent(this.config.sessionId) + "/dataobj/shinytest?w=" + encodeURIComponent(this.config.workerId) + "&nonce=" + randomId();
+    };
+  }).call(ShinyApp.prototype);
 
-  this.onValueChange = function(el, data) {
-    this.clearError(el);
-    this.renderValue(el, data);
-  };
-  this.onValueError = function(el, err) {
-    this.renderError(el, err);
-  };
-  this.renderError = function(el, err) {
-    this.clearError(el);
-    if (err.message === '') {
-      // not really error, but we just need to wait (e.g. action buttons)
-      $(el).empty();
-      return;
-    }
-    var errClass = 'shiny-output-error';
-    if (err.type !== null) {
-      // use the classes of the error condition as CSS class names
-      errClass = errClass + ' ' + $.map(asArray(err.type), function(type) {
-        return errClass + '-' + type;
-      }).join(' ');
-    }
-    $(el).addClass(errClass).text(err.message);
-  };
-  this.clearError = function(el) {
-    $(el).attr('class', function(i, c) {
-      return c.replace(/(^|\s)shiny-output-error\S*/g, '');
-    });
-  };
-  this.showProgress = function(el, show) {
-    var RECALC_CLASS = 'recalculating';
-    if (show)
-      $(el).addClass(RECALC_CLASS);
-    else
-      $(el).removeClass(RECALC_CLASS);
-  };
-}).call(OutputBinding.prototype);
+  exports.showReconnectDialog = function () {
+    var reconnectTime = null;
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/output_binding_text.js
-
-var textOutputBinding = new OutputBinding();
-$.extend(textOutputBinding, {
-  find: function(scope) {
-    return $(scope).find('.shiny-text-output');
-  },
-  renderValue: function(el, data) {
-    $(el).text(data);
-  }
-});
-outputBindings.register(textOutputBinding, 'shiny.textOutput');
+    function updateTime() {
+      var $time = $("#shiny-reconnect-time");
+      // If the time has been removed, exit and don't reschedule this function.
+      if ($time.length === 0) return;
 
+      var seconds = Math.floor((reconnectTime - new Date().getTime()) / 1000);
+      if (seconds > 0) {
+        $time.text(" in " + seconds + "s");
+      } else {
+        $time.text("...");
+      }
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/output_binding_image.js
-
-var imageOutputBinding = new OutputBinding();
-$.extend(imageOutputBinding, {
-  find: function(scope) {
-    return $(scope).find('.shiny-image-output, .shiny-plot-output');
-  },
-  renderValue: function(el, data) {
-    // The overall strategy:
-    // * Clear out existing image and event handlers.
-    // * Create new image.
-    // * Create various event handlers.
-    // * Bind those event handlers to events.
-    // * Insert the new image.
-
-    var outputId = this.getId(el);
-
-    var $el = $(el);
-    // Load the image before emptying, to minimize flicker
-    var img = null;
-
-    // Remove event handlers that were added in previous renderValue()
-    $el.off('.image_output');
-    // Trigger custom 'remove' event for any existing images in the div
-    $el.find('img').trigger('remove');
-
-    if (!data) {
-      $el.empty();
-      return;
+      // Reschedule this function after 1 second
+      setTimeout(updateTime, 1000);
     }
 
-    // If value is undefined, return alternate. Sort of like ||, except it won't
-    // return alternate for other falsy values (0, false, null).
-    function OR(value, alternate) {
-      if (value === undefined) return alternate;
-      return value;
-    }
+    return function (delay) {
+      reconnectTime = new Date().getTime() + delay;
 
-    var opts = {
-      clickId: $el.data('click-id'),
-      clickClip: OR(strToBool($el.data('click-clip')), true),
-
-      dblclickId: $el.data('dblclick-id'),
-      dblclickClip: OR(strToBool($el.data('dblclick-clip')), true),
-      dblclickDelay: OR($el.data('dblclick-delay'), 400),
-
-      hoverId: $el.data('hover-id'),
-      hoverClip: OR(strToBool($el.data('hover-clip')), true),
-      hoverDelayType: OR($el.data('hover-delay-type'), 'debounce'),
-      hoverDelay: OR($el.data('hover-delay'), 300),
-      hoverNullOutside: OR(strToBool($el.data('hover-null-outside')), false),
-
-      brushId: $el.data('brush-id'),
-      brushClip: OR(strToBool($el.data('brush-clip')), true),
-      brushDelayType: OR($el.data('brush-delay-type'), 'debounce'),
-      brushDelay: OR($el.data('brush-delay'), 300),
-      brushFill: OR($el.data('brush-fill'), '#666'),
-      brushStroke: OR($el.data('brush-stroke'), '#000'),
-      brushOpacity: OR($el.data('brush-opacity'), 0.3),
-      brushDirection: OR($el.data('brush-direction'), 'xy'),
-      brushResetOnNew: OR(strToBool($el.data('brush-reset-on-new')), false),
-
-      coordmap: data.coordmap
-    };
+      // If there's already a reconnect dialog, don't add another
+      if ($('#shiny-reconnect-text').length > 0) return;
 
-    img = document.createElement('img');
-    // Copy items from data to img. This should include 'src'
-    $.each(data, function(key, value) {
-      if (value !== null)
-        img.setAttribute(key, value);
-    });
+      var html = '<span id="shiny-reconnect-text">Attempting to reconnect</span>' + '<span id="shiny-reconnect-time"></span>';
+      var action = '<a id="shiny-reconnect-now" href="#" onclick="Shiny.shinyapp.reconnect();">Try now</a>';
 
-    var $img = $(img);
+      exports.notifications.show({
+        id: "reconnect",
+        html: html,
+        action: action,
+        duration: null,
+        closeButton: false,
+        type: 'warning'
+      });
 
-    if (!opts.coordmap)
-      opts.coordmap = [];
+      updateTime();
+    };
+  }();
 
-    imageutils.initCoordmap($el, opts.coordmap);
+  exports.hideReconnectDialog = function () {
+    exports.notifications.remove("reconnect");
+  };
 
-    // This object listens for mousedowns, and triggers mousedown2 and dblclick2
-    // events as appropriate.
-    var clickInfo = imageutils.createClickInfo($el, opts.dblclickId, opts.dblclickDelay);
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/notifications.js
+
+  exports.notifications = function () {
+
+    // Milliseconds to fade in or out
+    var fadeDuration = 250;
+
+    function show() {
+      var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+      var _ref$html = _ref.html;
+      var html = _ref$html === undefined ? '' : _ref$html;
+      var _ref$action = _ref.action;
+      var action = _ref$action === undefined ? '' : _ref$action;
+      var _ref$deps = _ref.deps;
+      var deps = _ref$deps === undefined ? [] : _ref$deps;
+      var _ref$duration = _ref.duration;
+      var duration = _ref$duration === undefined ? 5000 : _ref$duration;
+      var _ref$id = _ref.id;
+      var id = _ref$id === undefined ? null : _ref$id;
+      var _ref$closeButton = _ref.closeButton;
+      var closeButton = _ref$closeButton === undefined ? true : _ref$closeButton;
+      var _ref$type = _ref.type;
+      var type = _ref$type === undefined ? null : _ref$type;
+
+      if (!id) id = randomId();
+
+      // Create panel if necessary
+      _createPanel();
+
+      // Get existing DOM element for this ID, or create if needed.
+      var $notification = _get(id);
+      if ($notification.length === 0) $notification = _create(id);
+
+      // Render html and dependencies
+      var newHtml = '<div class="shiny-notification-content-text">' + html + '</div>' + ('<div class="shiny-notification-content-action">' + action + '</div>');
+      var $content = $notification.find('.shiny-notification-content');
+      exports.renderContent($content, { html: newHtml, deps: deps });
+
+      // Remove any existing classes of the form 'shiny-notification-xxxx'.
+      // The xxxx would be strings like 'warning'.
+      var classes = $notification.attr('class').split(/\s+/).filter(function (cls) {
+        return cls.match(/^shiny-notification-/);
+      }).join(' ');
+      $notification.removeClass(classes);
+
+      // Add class. 'default' means no additional CSS class.
+      if (type && type !== 'default') $notification.addClass('shiny-notification-' + type);
+
+      // Make sure that the presence/absence of close button matches with value
+      // of `closeButton`.
+      var $close = $notification.find('.shiny-notification-close');
+      if (closeButton && $close.length === 0) {
+        $notification.append('<div class="shiny-notification-close">×</div>');
+      } else if (!closeButton && $close.length !== 0) {
+        $close.remove();
+      }
 
-    $el.on('mousedown.image_output', clickInfo.mousedown);
+      // If duration was provided, schedule removal. If not, clear existing
+      // removal callback (this happens if a message was first added with
+      // a duration, and then updated with no duration).
+      if (duration) _addRemovalCallback(id, duration);else _clearRemovalCallback(id);
 
-    if (browser.isIE && browser.IEVersion === 8) {
-      $el.on('dblclick.image_output', clickInfo.dblclickIE8);
+      return id;
     }
 
-    // ----------------------------------------------------------
-    // Register the various event handlers
-    // ----------------------------------------------------------
-    if (opts.clickId) {
-      var clickHandler = imageutils.createClickHandler(opts.clickId,
-        opts.clickClip, opts.coordmap);
-      $el.on('mousedown2.image_output', clickHandler.mousedown);
-
-      // When img is removed, do housekeeping: clear $el's mouse listener and
-      // call the handler's onRemoveImg callback.
-      $img.on('remove', clickHandler.onRemoveImg);
-    }
+    function remove(id) {
+      _get(id).fadeOut(fadeDuration, function () {
 
-    if (opts.dblclickId) {
-      // We'll use the clickHandler's mousedown function, but register it to
-      // our custom 'dblclick2' event.
-      var dblclickHandler = imageutils.createClickHandler(opts.dblclickId,
-        opts.clickClip, opts.coordmap);
-      $el.on('dblclick2.image_output', dblclickHandler.mousedown);
+        exports.unbindAll(this);
+        $(this).remove();
 
-      $img.on('remove', dblclickHandler.onRemoveImg);
+        // If no more notifications, remove the panel from the DOM.
+        if (_ids().length === 0) {
+          _getPanel().remove();
+        }
+      });
     }
 
-    if (opts.hoverId) {
-      var hoverHandler = imageutils.createHoverHandler(opts.hoverId,
-        opts.hoverDelay, opts.hoverDelayType, opts.hoverClip,
-        opts.hoverNullOutside, opts.coordmap);
-      $el.on('mousemove.image_output', hoverHandler.mousemove);
-      $el.on('mouseout.image_output', hoverHandler.mouseout);
-
-      $img.on('remove', hoverHandler.onRemoveImg);
+    // Returns an individual notification DOM object (wrapped in jQuery).
+    function _get(id) {
+      if (!id) return null;
+      return _getPanel().find('#shiny-notification-' + $escape(id));
     }
 
-    if (opts.brushId) {
-      // Make image non-draggable (Chrome, Safari)
-      $img.css('-webkit-user-drag', 'none');
-      // Firefox, IE<=10
-      $img.on('dragstart', function() { return false; });
-
-      // Disable selection of image and text when dragging in IE<=10
-      $el.on('selectstart.image_output', function() { return false; });
-
-      var brushHandler = imageutils.createBrushHandler(opts.brushId, $el, opts,
-        opts.coordmap, outputId);
-      $el.on('mousedown.image_output', brushHandler.mousedown);
-      $el.on('mousemove.image_output', brushHandler.mousemove);
-
-      $img.on('remove', brushHandler.onRemoveImg);
+    // Return array of all notification IDs
+    function _ids() {
+      return _getPanel().find('.shiny-notification').map(function () {
+        return this.id.replace(/shiny-notification-/, '');
+      }).get();
     }
 
-    if (opts.clickId || opts.dblclickId || opts.hoverId || opts.brushId) {
-      $el.addClass('crosshair');
+    // Returns the notification panel DOM object (wrapped in jQuery).
+    function _getPanel() {
+      return $('#shiny-notification-panel');
     }
 
-    // Remove all elements except brush, usually image plus error messages.
-    // These extra contortions are needed to select the bare text of error
-    // message.
-    $el.contents().filter(function() {
-      return this.id !== el.id + '_brush';
-    }).remove();
+    // Create notifications panel and return the jQuery object. If the DOM
+    // element already exists, just return it.
+    function _createPanel() {
+      var $panel = _getPanel();
 
-    if (img)
-      $el.append(img);
+      if ($panel.length > 0) return $panel;
 
-    if (data.error)
-      console.log('Error on server extracting coordmap: ' + data.error);
-  }
-});
-outputBindings.register(imageOutputBinding, 'shiny.imageOutput');
+      $('body').append('<div id="shiny-notification-panel">');
 
+      return $panel;
+    }
 
-var imageutils = {};
+    // Create a notification DOM element and return the jQuery object. If the
+    // DOM element already exists for the ID, just return it without creating.
+    function _create(id) {
+      var $notification = _get(id);
 
+      if ($notification.length === 0) {
+        $notification = $('<div id="shiny-notification-' + id + '" class="shiny-notification">' + '<div class="shiny-notification-close">×</div>' + '<div class="shiny-notification-content"></div>' + '</div>');
 
-// Modifies the panel objects in a coordmap, adding scale(), scaleInv(),
-// and clip() functions to each one.
-imageutils.initPanelScales = function(coordmap) {
-  // Map a value x from a domain to a range. If clip is true, clip it to the
-  // range.
-  function mapLinear(x, domainMin, domainMax, rangeMin, rangeMax, clip) {
-    // By default, clip to range
-    clip = clip || true;
+        $notification.find('.shiny-notification-close').on('click', function (e) {
+          e.preventDefault();
+          e.stopPropagation();
+          remove(id);
+        });
 
-    var factor = (rangeMax - rangeMin) / (domainMax - domainMin);
-    var val = x - domainMin;
-    var newval = (val * factor) + rangeMin;
+        _getPanel().append($notification);
+      }
 
-    if (clip) {
-      var max = Math.max(rangeMax, rangeMin);
-      var min = Math.min(rangeMax, rangeMin);
-      if (newval > max)
-        newval = max;
-      else if (newval < min)
-        newval = min;
+      return $notification;
     }
-    return newval;
-  }
 
-  // Create scale and inverse-scale functions for a single direction (x or y).
-  function scaler1D(domainMin, domainMax, rangeMin, rangeMax, logbase) {
-    return {
-      scale: function(val, clip) {
-        if (logbase)
-          val = Math.log(val) / Math.log(logbase);
-        return mapLinear(val, domainMin, domainMax, rangeMin, rangeMax, clip);
-      },
+    // Add a callback to remove a notification after a delay in ms.
+    function _addRemovalCallback(id, delay) {
+      // If there's an existing removalCallback, clear it before adding the new
+      // one.
+      _clearRemovalCallback(id);
 
-      scaleInv: function(val, clip) {
-        var res = mapLinear(val, rangeMin, rangeMax, domainMin, domainMax, clip);
-        if (logbase)
-          res = Math.pow(logbase, res);
-        return res;
+      // Attach new removal callback
+      var removalCallback = setTimeout(function () {
+        remove(id);
+      }, delay);
+      _get(id).data('removalCallback', removalCallback);
+    }
+
+    // Clear a removal callback from a notification, if present.
+    function _clearRemovalCallback(id) {
+      var $notification = _get(id);
+      var oldRemovalCallback = $notification.data('removalCallback');
+      if (oldRemovalCallback) {
+        clearTimeout(oldRemovalCallback);
       }
-    };
-  }
+    }
 
-  // Modify panel, adding scale and inverse-scale functions that take objects
-  // like {x:1, y:3}, and also add clip function.
-  function addScaleFuns(panel) {
-    var d = panel.domain;
-    var r = panel.range;
-    var xlog = (panel.log && panel.log.x) ? panel.log.x : null;
-    var ylog = (panel.log && panel.log.y) ? panel.log.y : null;
-    var xscaler = scaler1D(d.left, d.right, r.left, r.right, xlog);
-    var yscaler = scaler1D(d.bottom, d.top, r.bottom, r.top, ylog);
-
-    panel.scale = function(val, clip) {
-      return {
-        x: xscaler.scale(val.x, clip),
-        y: yscaler.scale(val.y, clip)
-      };
+    return {
+      show: show,
+      remove: remove
     };
+  }();
+
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/modal.js
+
+  exports.modal = {
+
+    // Show a modal dialog. This is meant to handle two types of cases: one is
+    // that the content is a Bootstrap modal dialog, and the other is that the
+    // content is non-Bootstrap. Bootstrap modals require some special handling,
+    // which is coded in here.
+    show: function show() {
+      var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+      var _ref2$html = _ref2.html;
+      var html = _ref2$html === undefined ? '' : _ref2$html;
+      var _ref2$deps = _ref2.deps;
+      var deps = _ref2$deps === undefined ? [] : _ref2$deps;
+
+
+      // If there was an existing Bootstrap modal, then there will be a modal-
+      // backdrop div that was added outside of the modal wrapper, and it must be
+      // removed; otherwise there can be multiple of these divs.
+      $('.modal-backdrop').remove();
+
+      // Get existing wrapper DOM element, or create if needed.
+      var $modal = $('#shiny-modal-wrapper');
+      if ($modal.length === 0) {
+        $modal = $('<div id="shiny-modal-wrapper"></div>');
+        $('body').append($modal);
+
+        // If the wrapper's content is a Bootstrap modal, then when the inner
+        // modal is hidden, remove the entire thing, including wrapper.
+        $modal.on('hidden.bs.modal', function (e) {
+          if (e.target === $("#shiny-modal")[0]) {
+            exports.unbindAll($modal);
+            $modal.remove();
+          }
+        });
+      }
 
-    panel.scaleInv = function(val, clip) {
-      return {
-        x: xscaler.scaleInv(val.x, clip),
-        y: yscaler.scaleInv(val.y, clip)
-      };
-    };
+      // Set/replace contents of wrapper with html.
+      exports.renderContent($modal, { html: html, deps: deps });
+    },
 
-    // Given a scaled offset (in pixels), clip it to the nearest panel region.
-    panel.clip = function(offset) {
-      var newOffset = {
-        x: offset.x,
-        y: offset.y
-      };
+    remove: function remove() {
+      var $modal = $('#shiny-modal-wrapper');
 
-      var bounds = panel.range;
+      // Look for a Bootstrap modal and if present, trigger hide event. This will
+      // trigger the hidden.bs.modal callback that we set in show(), which unbinds
+      // and removes the element.
+      if ($modal.find('.modal').length > 0) {
+        $modal.find('.modal').modal('hide');
+      } else {
+        // If not a Bootstrap modal dialog, simply unbind and remove it.
+        exports.unbindAll($modal);
+        $modal.remove();
+      }
+    }
+  };
 
-      if      (offset.x > bounds.right)  newOffset.x = bounds.right;
-      else if (offset.x < bounds.left)   newOffset.x = bounds.left;
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/file_processor.js
 
-      if      (offset.y > bounds.bottom) newOffset.y = bounds.bottom;
-      else if (offset.y < bounds.top)    newOffset.y = bounds.top;
+  // 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 FileProcessor(files) {
+    this.files = files;
+    this.fileIndex = -1;
+    // Currently need to use small chunk size because R-Websockets can't
+    // handle continuation frames
+    this.aborted = false;
+    this.completed = false;
 
-      return newOffset;
-    };
-  }
+    // TODO: Register error/abort callbacks
 
-  // Add the functions to each panel object.
-  for (var i=0; i<coordmap.length; i++) {
-    var panel = coordmap[i];
-    addScaleFuns(panel);
-  }
-};
-
-
-// This adds functions to the coordmap object to handle various
-// coordinate-mapping tasks, and send information to the server.
-// The input coordmap is an array of objects, each of which represents a panel.
-// coordmap must be an array, even if empty, so that it can be modified in
-// place; when empty, we add a dummy panel to the array.
-// It also calls initPanelScales, which modifies each panel object to have
-// scale, scaleInv, and clip functions.
-imageutils.initCoordmap = function($el, coordmap) {
-  var el = $el[0];
-
-  // If we didn't get any panels, create a dummy one where the domain and range
-  // are simply the pixel dimensions.
-  // that we modify.
-  if (coordmap.length === 0) {
-    var bounds = {
-      top: 0,
-      left: 0,
-      right: el.clientWidth - 1,
-      bottom: el.clientHeight - 1
+    this.$run();
+  };
+  (function () {
+    // Begin callbacks. Subclassers/cloners may override any or all of these.
+    this.onBegin = function (files, cont) {
+      setTimeout(cont, 0);
     };
-
-    coordmap[0] = {
-      domain: bounds,
-      range: bounds,
-      mapping: {}
+    this.onFile = function (file, cont) {
+      setTimeout(cont, 0);
     };
-  }
+    this.onComplete = function () {};
+    this.onAbort = function () {};
+    // End callbacks
 
-  // Add scaling functions to each panel
-  imageutils.initPanelScales(coordmap);
+    // Aborts processing, unless it's already completed
+    this.abort = function () {
+      if (this.completed || this.aborted) return;
 
-  // Firefox doesn't have offsetX/Y, so we need to use an alternate
-  // method of calculation for it. Even though other browsers do have
-  // offsetX/Y, we need to calculate relative to $el, because sometimes the
-  // mouse event can come with offset relative to other elements on the
-  // page. This happens when the event listener is bound to, say, window.
-  coordmap.mouseOffset = function(mouseEvent) {
-    var offset = $el.offset();
-    return {
-      x: mouseEvent.pageX - offset.left,
-      y: mouseEvent.pageY - offset.top
+      this.aborted = true;
+      this.onAbort();
     };
-  };
 
-  // Given two sets of x/y coordinates, return an object representing the
-  // min and max x and y values. (This could be generalized to any number
-  // of points).
-  coordmap.findBox = function(offset1, offset2) {
-    return {
-      xmin: Math.min(offset1.x, offset2.x),
-      xmax: Math.max(offset1.x, offset2.x),
-      ymin: Math.min(offset1.y, offset2.y),
-      ymax: Math.max(offset1.y, offset2.y)
+    // 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();
+      };
     };
-  };
-
 
-  // Shift an array of values so that they are within a min and max.
-  // The vals will be shifted so that they maintain the same spacing
-  // internally. If the range in vals is larger than the range of
-  // min and max, the result might not make sense.
-  coordmap.shiftToRange = function(vals, min, max) {
-    if (!(vals instanceof Array))
-      vals = [vals];
-
-    var maxval = Math.max.apply(null, vals);
-    var minval = Math.min.apply(null, vals);
-    var shiftAmount = 0;
-    if (maxval > max) {
-      shiftAmount = max - maxval;
-    } else if (minval < min) {
-      shiftAmount = min - minval;
-    }
-
-    var newvals = [];
-    for (var i=0; i<vals.length; i++) {
-      newvals[i] = vals[i] + shiftAmount;
-    }
-    return newvals;
-  };
+    // 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 () {
 
-  // Given an offset, return an object representing which panel it's in. The
-  // `expand` argument tells it to expand the panel area by that many pixels.
-  // It's possible for an offset to be within more than one panel, because
-  // of the `expand` value. If that's the case, find the nearest panel.
-  coordmap.getPanel = function(offset, expand) {
-    expand = expand || 0;
-
-    var x = offset.x;
-    var y = offset.y;
-
-    var matches = []; // Panels that match
-    var dists = [];   // Distance of offset to each matching panel
-    var b;
-    for (var i=0; i<coordmap.length; i++) {
-      b = coordmap[i].range;
-
-      if (x <= b.right  + expand &&
-          x >= b.left   - expand &&
-          y <= b.bottom + expand &&
-          y >= b.top    - expand)
-      {
-        matches.push(coordmap[i]);
-
-        // Find distance from edges for x and y
-        var xdist = 0;
-        var ydist = 0;
-        if (x > b.right && x <= b.right + expand) {
-          xdist = x - b.right;
-        } else if (x < b.left && x >= b.left - expand) {
-          xdist = x - b.left;
-        }
-        if (y > b.bottom && y <= b.bottom + expand) {
-          ydist = y - b.bottom;
-        } else if (y < b.top && y >= b.top - expand) {
-          ydist = y - b.top;
-        }
+      if (this.aborted || this.completed) return;
 
-        // Cartesian distance
-        dists.push(Math.sqrt( Math.pow(xdist, 2) + Math.pow(ydist, 2) ));
+      if (this.fileIndex < 0) {
+        // Haven't started yet--begin
+        this.fileIndex = 0;
+        this.onBegin(this.files, this.$getRun());
+        return;
       }
-    }
 
-    if (matches.length) {
-      // Find shortest distance
-      var min_dist = Math.min.apply(null, dists);
-      for (i=0; i<matches.length; i++) {
-        if (dists[i] === min_dist) {
-          return matches[i];
-        }
+      if (this.fileIndex === this.files.length) {
+        // Just ended
+        this.completed = true;
+        this.onComplete();
+        return;
       }
-    }
 
-    return null;
-  };
+      // 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.
 
-  // Is an offset in a panel? If supplied, `expand` tells us to expand the
-  // panels by that many pixels in all directions.
-  coordmap.isInPanel = function(offset, expand) {
-    expand = expand || 0;
+      var file = this.files[this.fileIndex++];
+      this.onFile(file, this.$getRun());
+    };
+  }).call(FileProcessor.prototype);
 
-    if (coordmap.getPanel(offset, expand))
-      return true;
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/binding_registry.js
 
-    return false;
+  var BindingRegistry = function BindingRegistry() {
+    this.bindings = [];
+    this.bindingNames = {};
   };
+  (function () {
+    this.register = function (binding, bindingName, priority) {
+      var bindingObj = { binding: binding, priority: priority || 0 };
+      this.bindings.unshift(bindingObj);
+      if (bindingName) {
+        this.bindingNames[bindingName] = bindingObj;
+        binding.name = bindingName;
+      }
+    };
+    this.setPriority = function (bindingName, priority) {
+      var bindingObj = this.bindingNames[bindingName];
+      if (!bindingObj) throw "Tried to set priority on unknown binding " + bindingName;
+      bindingObj.priority = priority || 0;
+    };
+    this.getPriority = function (bindingName) {
+      var bindingObj = this.bindingNames[bindingName];
+      if (!bindingObj) return false;
+      return bindingObj.priority;
+    };
+    this.getBindings = function () {
+      // Sort the bindings. The ones with higher priority are consulted
+      // first; ties are broken by most-recently-registered.
+      return mergeSort(this.bindings, function (a, b) {
+        return b.priority - a.priority;
+      });
+    };
+  }).call(BindingRegistry.prototype);
 
-  // Returns a function that sends mouse coordinates, scaled to data space.
-  // If that function is passed a null event, it will send null.
-  coordmap.mouseCoordinateSender = function(inputId, clip, nullOutside) {
-    if (clip === undefined) clip = true;
-    if (nullOutside === undefined) nullOutside = false;
+  var inputBindings = exports.inputBindings = new BindingRegistry();
+  var outputBindings = exports.outputBindings = new BindingRegistry();
 
-    return function(e) {
-      if (e === null) {
-        exports.onInputChange(inputId, null);
-        return;
-      }
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/output_binding.js
 
-      var offset = coordmap.mouseOffset(e);
-      // If outside of plotting region
-      if (!coordmap.isInPanel(offset)) {
-        if (nullOutside) {
-          exports.onInputChange(inputId, null);
-          return;
-        }
-        if (clip)
-          return;
-      }
-      if (clip && !coordmap.isInPanel(offset)) return;
-
-      var panel = coordmap.getPanel(offset);
-      var coords = panel.scaleInv(offset);
-
-      // Add the panel (facet) variables, if present
-      $.extend(coords, panel.panel_vars);
-
-      // Add variable name mappings
-      coords.mapping = panel.mapping;
+  var OutputBinding = exports.OutputBinding = function () {};
+  (function () {
+    // Returns a jQuery object or element array that contains the
+    // descendants of scope that match this binding
+    this.find = function (scope) {
+      throw "Not implemented";
+    };
 
-      // Add scaling information
-      coords.domain = panel.domain;
-      coords.range  = panel.range;
-      coords.log    = panel.log;
+    this.getId = function (el) {
+      return el['data-input-id'] || el.id;
+    };
 
-      coords[".nonce"] = Math.random();
-      exports.onInputChange(inputId, coords);
+    this.onValueChange = function (el, data) {
+      this.clearError(el);
+      this.renderValue(el, data);
     };
-  };
-};
-
-
-// This object provides two public event listeners: mousedown, and
-// dblclickIE8.
-// We need to make sure that, when the image is listening for double-
-// clicks, that a double-click doesn't trigger two click events. We'll
-// trigger custom mousedown2 and dblclick2 events with this mousedown
-// listener.
-imageutils.createClickInfo = function($el, dblclickId, dblclickDelay) {
-  var clickCount = 0;
-  var clickTimer = null;
-  var pending_e = null;    // A pending mousedown2 event
-
-  // Create a new event of type eventType (like 'mousedown2'), and trigger
-  // it with the information stored in this.e.
-  function triggerEvent(newEventType, e) {
-    // Extract important info from e and construct a new event with type
-    // eventType.
-    var e2 = $.Event(newEventType, {
-      which:   e.which,
-      pageX:   e.pageX,
-      pageY:   e.pageY,
-      offsetX: e.offsetX,
-      offsetY: e.offsetY
-    });
+    this.onValueError = function (el, err) {
+      this.renderError(el, err);
+    };
+    this.renderError = function (el, err) {
+      this.clearError(el);
+      if (err.message === '') {
+        // not really error, but we just need to wait (e.g. action buttons)
+        $(el).empty();
+        return;
+      }
+      var errClass = 'shiny-output-error';
+      if (err.type !== null) {
+        // use the classes of the error condition as CSS class names
+        errClass = errClass + ' ' + $.map(asArray(err.type), function (type) {
+          return errClass + '-' + type;
+        }).join(' ');
+      }
+      $(el).addClass(errClass).text(err.message);
+    };
+    this.clearError = function (el) {
+      $(el).attr('class', function (i, c) {
+        return c.replace(/(^|\s)shiny-output-error\S*/g, '');
+      });
+    };
+    this.showProgress = function (el, show) {
+      var RECALC_CLASS = 'recalculating';
+      if (show) $(el).addClass(RECALC_CLASS);else $(el).removeClass(RECALC_CLASS);
+    };
+  }).call(OutputBinding.prototype);
 
-    $el.trigger(e2);
-  }
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/output_binding_text.js
 
-  function triggerPendingMousedown2() {
-    // It's possible that between the scheduling of a mousedown2 and the
-    // time this callback is executed, someone else triggers a
-    // mousedown2, so check for that.
-    if (pending_e) {
-      triggerEvent('mousedown2', pending_e);
-      pending_e = null;
+  var textOutputBinding = new OutputBinding();
+  $.extend(textOutputBinding, {
+    find: function find(scope) {
+      return $(scope).find('.shiny-text-output');
+    },
+    renderValue: function renderValue(el, data) {
+      $(el).text(data);
     }
-  }
+  });
+  outputBindings.register(textOutputBinding, 'shiny.textOutput');
 
-  // Set a timer to trigger a mousedown2 event, using information from the
-  // last recorded mousdown event.
-  function scheduleMousedown2(e) {
-    pending_e = e;
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/output_binding_image.js
 
-    clickTimer = setTimeout(function() {
-      triggerPendingMousedown2();
-    }, dblclickDelay);
-  }
+  var imageOutputBinding = new OutputBinding();
+  $.extend(imageOutputBinding, {
+    find: function find(scope) {
+      return $(scope).find('.shiny-image-output, .shiny-plot-output');
+    },
+    renderValue: function renderValue(el, data) {
+      // The overall strategy:
+      // * Clear out existing image and event handlers.
+      // * Create new image.
+      // * Create various event handlers.
+      // * Bind those event handlers to events.
+      // * Insert the new image.
+
+      var outputId = this.getId(el);
+
+      var $el = $(el);
+      var img;
+
+      // Remove event handlers that were added in previous renderValue()
+      $el.off('.image_output');
+
+      // Get existing img element if present.
+      var $img = $el.find('img');
+
+      if ($img.length === 0) {
+        // If a img element is not already present, that means this is either
+        // the first time renderValue() has been called, or this is after an
+        // error.
+        img = document.createElement('img');
+        $el.append(img);
+        $img = $(img);
+      } else {
+        // Trigger custom 'reset' event for any existing images in the div
+        img = $img[0];
+        $img.trigger('reset');
+      }
 
-  function mousedown(e) {
-    // Listen for left mouse button only
-    if (e.which !== 1) return;
+      if (!data) {
+        $el.empty();
+        return;
+      }
 
-    // If no dblclick listener, immediately trigger a mousedown2 event.
-    if (!dblclickId) {
-      triggerEvent('mousedown2', e);
-      return;
-    }
+      // If value is undefined, return alternate. Sort of like ||, except it won't
+      // return alternate for other falsy values (0, false, null).
+      function OR(value, alternate) {
+        if (value === undefined) return alternate;
+        return value;
+      }
 
-    // If there's a dblclick listener, make sure not to count this as a
-    // click on the first mousedown; we need to wait for the dblclick
-    // delay before we can be sure this click was a single-click.
-    if (pending_e === null) {
-      scheduleMousedown2(e);
+      var opts = {
+        clickId: $el.data('click-id'),
+        clickClip: OR(strToBool($el.data('click-clip')), true),
+
+        dblclickId: $el.data('dblclick-id'),
+        dblclickClip: OR(strToBool($el.data('dblclick-clip')), true),
+        dblclickDelay: OR($el.data('dblclick-delay'), 400),
+
+        hoverId: $el.data('hover-id'),
+        hoverClip: OR(strToBool($el.data('hover-clip')), true),
+        hoverDelayType: OR($el.data('hover-delay-type'), 'debounce'),
+        hoverDelay: OR($el.data('hover-delay'), 300),
+        hoverNullOutside: OR(strToBool($el.data('hover-null-outside')), false),
+
+        brushId: $el.data('brush-id'),
+        brushClip: OR(strToBool($el.data('brush-clip')), true),
+        brushDelayType: OR($el.data('brush-delay-type'), 'debounce'),
+        brushDelay: OR($el.data('brush-delay'), 300),
+        brushFill: OR($el.data('brush-fill'), '#666'),
+        brushStroke: OR($el.data('brush-stroke'), '#000'),
+        brushOpacity: OR($el.data('brush-opacity'), 0.3),
+        brushDirection: OR($el.data('brush-direction'), 'xy'),
+        brushResetOnNew: OR(strToBool($el.data('brush-reset-on-new')), false),
+
+        coordmap: data.coordmap
+      };
 
-    } else {
-      clearTimeout(clickTimer);
+      // Copy items from data to img. Don't set the coordmap as an attribute.
+      $.each(data, function (key, value) {
+        if (value === null || key === 'coordmap') {
+          return;
+        }
+        img.setAttribute(key, value);
+      });
 
-      // If second click is too far away, it doesn't count as a double
-      // click. Instead, immediately trigger a mousedown2 for the previous
-      // click, and set this click as a new first click.
-      if (pending_e &&
-          Math.abs(pending_e.offsetX - e.offsetX) > 2 ||
-          Math.abs(pending_e.offsetY - e.offsetY) > 2) {
+      // Unset any attributes in the current img that were not provided in the
+      // new data.
+      for (var i = 0; i < img.attributes.length; i++) {
+        var attrib = img.attributes[i];
+        // Need to check attrib.specified on IE because img.attributes contains
+        // all possible attributes on IE.
+        if (attrib.specified && !data.hasOwnProperty(attrib.name)) {
+          img.removeAttribute(attrib.name);
+        }
+      }
 
-        triggerPendingMousedown2();
-        scheduleMousedown2(e);
+      if (!opts.coordmap) opts.coordmap = [];
 
-      } else {
-        // The second click was close to the first one. If it happened
-        // within specified delay, trigger our custom 'dblclick2' event.
-        pending_e = null;
-        triggerEvent('dblclick2', e);
-      }
-    }
-  }
+      imageutils.initCoordmap($el, opts.coordmap);
 
-  // IE8 needs a special hack because when you do a double-click it doesn't
-  // trigger the click event twice - it directly triggers dblclick.
-  function dblclickIE8(e) {
-    e.which = 1;   // In IE8, e.which is 0 instead of 1. ???
-    triggerEvent('dblclick2', e);
-  }
+      // This object listens for mousedowns, and triggers mousedown2 and dblclick2
+      // events as appropriate.
+      var clickInfo = imageutils.createClickInfo($el, opts.dblclickId, opts.dblclickDelay);
 
-  return {
-    mousedown: mousedown,
-    dblclickIE8: dblclickIE8
-  };
-};
+      $el.on('mousedown.image_output', clickInfo.mousedown);
 
+      if (browser.isIE && browser.IEVersion === 8) {
+        $el.on('dblclick.image_output', clickInfo.dblclickIE8);
+      }
 
-// ----------------------------------------------------------
-// Handler creators for click, hover, brush.
-// Each of these returns an object with a few public members. These public
-// members are callbacks that are meant to be bound to events on $el with
-// the same name (like 'mousedown').
-// ----------------------------------------------------------
+      // ----------------------------------------------------------
+      // Register the various event handlers
+      // ----------------------------------------------------------
+      if (opts.clickId) {
+        var clickHandler = imageutils.createClickHandler(opts.clickId, opts.clickClip, opts.coordmap);
+        $el.on('mousedown2.image_output', clickHandler.mousedown);
 
-imageutils.createClickHandler = function(inputId, clip, coordmap) {
-  var clickInfoSender = coordmap.mouseCoordinateSender(inputId, clip);
+        // When img is reset, do housekeeping: clear $el's mouse listener and
+        // call the handler's onResetImg callback.
+        $img.on('reset', clickHandler.onResetImg);
+      }
 
-  return {
-    mousedown: function(e) {
-      // Listen for left mouse button only
-      if (e.which !== 1) return;
-      clickInfoSender(e);
-    },
-    onRemoveImg: function() { clickInfoSender(null); }
-  };
-};
-
-
-imageutils.createHoverHandler = function(inputId, delay, delayType, clip,
-  nullOutside, coordmap)
-{
-  var sendHoverInfo = coordmap.mouseCoordinateSender(inputId, clip, nullOutside);
-
-  var hoverInfoSender;
-  if (delayType === 'throttle')
-    hoverInfoSender = new Throttler(null, sendHoverInfo, delay);
-  else
-    hoverInfoSender = new Debouncer(null, sendHoverInfo, delay);
-
-  // What to do when mouse exits the image
-  var mouseout;
-  if (nullOutside)
-    mouseout = function() { hoverInfoSender.normalCall(null); };
-  else
-    mouseout = function() {};
-
-  return {
-    mousemove:   function(e) { hoverInfoSender.normalCall(e); },
-    mouseout: mouseout,
-    onRemoveImg: function()  { hoverInfoSender.immediateCall(null); }
-  };
-};
+      if (opts.dblclickId) {
+        // We'll use the clickHandler's mousedown function, but register it to
+        // our custom 'dblclick2' event.
+        var dblclickHandler = imageutils.createClickHandler(opts.dblclickId, opts.clickClip, opts.coordmap);
+        $el.on('dblclick2.image_output', dblclickHandler.mousedown);
+
+        $img.on('reset', dblclickHandler.onResetImg);
+      }
 
+      if (opts.hoverId) {
+        var hoverHandler = imageutils.createHoverHandler(opts.hoverId, opts.hoverDelay, opts.hoverDelayType, opts.hoverClip, opts.hoverNullOutside, opts.coordmap);
+        $el.on('mousemove.image_output', hoverHandler.mousemove);
+        $el.on('mouseout.image_output', hoverHandler.mouseout);
 
-// Returns a brush handler object. This has three public functions:
-// mousedown, mousemove, and onRemoveImg.
-imageutils.createBrushHandler = function(inputId, $el, opts, coordmap, outputId) {
-  // Parameter: expand the area in which a brush can be started, by this
-  // many pixels in all directions. (This should probably be a brush option)
-  var expandPixels = 20;
+        $img.on('reset', hoverHandler.onResetImg);
+      }
 
-  // Represents the state of the brush
-  var brush = imageutils.createBrush($el, opts, coordmap, expandPixels);
+      if (opts.brushId) {
+        // Make image non-draggable (Chrome, Safari)
+        $img.css('-webkit-user-drag', 'none');
+        // Firefox, IE<=10
+        $img.on('dragstart', function () {
+          return false;
+        });
 
-  // Brush IDs can span multiple image/plot outputs. When an output is brushed,
-  // if a brush with the same ID is active on a different image/plot, it must
-  // be dismissed (but without sending any data to the server). We implement
-  // this by sending the shiny-internal:brushed event to all plots, and letting
-  // each plot decide for itself what to do.
-  //
-  // The decision to have the event sent to each plot (as opposed to a single
-  // event triggered on, say, the document) was made to make cleanup easier;
-  // listening on an event on the document would prevent garbage collection
-  // of plot outputs that are removed from the document.
-  $el.on("shiny-internal:brushed.image_output", function(e, coords) {
-    // If the new brush shares our ID but not our output element ID, we
-    // need to clear our brush (if any).
-    if (coords.brushId === inputId && coords.outputId !== outputId) {
-      $el.data("mostRecentBrush", false);
-      brush.reset();
-    }
-  });
+        // Disable selection of image and text when dragging in IE<=10
+        $el.on('selectstart.image_output', function () {
+          return false;
+        });
 
-  // Set cursor to one of 7 styles. We need to set the cursor on the whole
-  // el instead of the brush div, because the brush div has
-  // 'pointer-events:none' so that it won't intercept pointer events.
-  // If `style` is null, don't add a cursor style.
-  function setCursorStyle(style) {
-    $el.removeClass('crosshair grabbable grabbing ns-resize ew-resize nesw-resize nwse-resize');
+        var brushHandler = imageutils.createBrushHandler(opts.brushId, $el, opts, opts.coordmap, outputId);
+        $el.on('mousedown.image_output', brushHandler.mousedown);
+        $el.on('mousemove.image_output', brushHandler.mousemove);
 
-    if (style) $el.addClass(style);
-  }
+        $img.on('reset', brushHandler.onResetImg);
+      }
 
-  function sendBrushInfo() {
-    var coords = brush.boundsData();
+      if (opts.clickId || opts.dblclickId || opts.hoverId || opts.brushId) {
+        $el.addClass('crosshair');
+      }
 
-    // We're in a new or reset state
-    if (isNaN(coords.xmin)) {
-      exports.onInputChange(inputId, null);
-      // Must tell other brushes to clear.
-      imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
-        brushId: inputId, outputId: null
-      });
-      return;
-    }
+      if (data.error) console.log('Error on server extracting coordmap: ' + data.error);
+    },
 
-    var panel = brush.getPanel();
+    renderError: function renderError(el, err) {
+      $(el).find('img').trigger('reset');
+      OutputBinding.prototype.renderError.call(this, el, err);
+    },
 
-    // Add the panel (facet) variables, if present
-    $.extend(coords, panel.panel_vars);
+    clearError: function clearError(el) {
+      // Remove all elements except img and the brush; this is usually just
+      // error messages.
+      $(el).contents().filter(function () {
+        return this.tagName !== "IMG" && this.id !== el.id + '_brush';
+      }).remove();
 
-    // Add variable name mappings
-    coords.mapping = panel.mapping;
+      OutputBinding.prototype.clearError.call(this, el);
+    }
+  });
+  outputBindings.register(imageOutputBinding, 'shiny.imageOutput');
+
+  var imageutils = {};
+
+  // Modifies the panel objects in a coordmap, adding scale(), scaleInv(),
+  // and clip() functions to each one.
+  imageutils.initPanelScales = function (coordmap) {
+    // Map a value x from a domain to a range. If clip is true, clip it to the
+    // range.
+    function mapLinear(x, domainMin, domainMax, rangeMin, rangeMax, clip) {
+      // By default, clip to range
+      clip = clip || true;
+
+      var factor = (rangeMax - rangeMin) / (domainMax - domainMin);
+      var val = x - domainMin;
+      var newval = val * factor + rangeMin;
+
+      if (clip) {
+        var max = Math.max(rangeMax, rangeMin);
+        var min = Math.min(rangeMax, rangeMin);
+        if (newval > max) newval = max;else if (newval < min) newval = min;
+      }
+      return newval;
+    }
 
-    // Add scaling information
-    coords.domain = panel.domain;
-    coords.range  = panel.range;
-    coords.log    = panel.log;
+    // Create scale and inverse-scale functions for a single direction (x or y).
+    function scaler1D(domainMin, domainMax, rangeMin, rangeMax, logbase) {
+      return {
+        scale: function scale(val, clip) {
+          if (logbase) val = Math.log(val) / Math.log(logbase);
+          return mapLinear(val, domainMin, domainMax, rangeMin, rangeMax, clip);
+        },
 
-    coords.direction = opts.brushDirection;
+        scaleInv: function scaleInv(val, clip) {
+          var res = mapLinear(val, rangeMin, rangeMax, domainMin, domainMax, clip);
+          if (logbase) res = Math.pow(logbase, res);
+          return res;
+        }
+      };
+    }
 
-    coords.brushId = inputId;
-    coords.outputId = outputId;
+    // Modify panel, adding scale and inverse-scale functions that take objects
+    // like {x:1, y:3}, and also add clip function.
+    function addScaleFuns(panel) {
+      var d = panel.domain;
+      var r = panel.range;
+      var xlog = panel.log && panel.log.x ? panel.log.x : null;
+      var ylog = panel.log && panel.log.y ? panel.log.y : null;
+      var xscaler = scaler1D(d.left, d.right, r.left, r.right, xlog);
+      var yscaler = scaler1D(d.bottom, d.top, r.bottom, r.top, ylog);
+
+      panel.scale = function (val, clip) {
+        return {
+          x: xscaler.scale(val.x, clip),
+          y: yscaler.scale(val.y, clip)
+        };
+      };
 
-    // Send data to server
-    exports.onInputChange(inputId, coords);
+      panel.scaleInv = function (val, clip) {
+        return {
+          x: xscaler.scaleInv(val.x, clip),
+          y: yscaler.scaleInv(val.y, clip)
+        };
+      };
 
-    $el.data("mostRecentBrush", true);
-    imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
-  }
+      // Given a scaled offset (in pixels), clip it to the nearest panel region.
+      panel.clip = function (offset) {
+        var newOffset = {
+          x: offset.x,
+          y: offset.y
+        };
 
-  var brushInfoSender;
-  if (opts.brushDelayType === 'throttle') {
-    brushInfoSender = new Throttler(null, sendBrushInfo, opts.brushDelay);
-  } else {
-    brushInfoSender = new Debouncer(null, sendBrushInfo, opts.brushDelay);
-  }
+        var bounds = panel.range;
 
-  function mousedown(e) {
-    // This can happen when mousedown inside the graphic, then mouseup
-    // outside, then mousedown inside. Just ignore the second
-    // mousedown.
-    if (brush.isBrushing() || brush.isDragging() || brush.isResizing()) return;
+        if (offset.x > bounds.right) newOffset.x = bounds.right;else if (offset.x < bounds.left) newOffset.x = bounds.left;
 
-    // Listen for left mouse button only
-    if (e.which !== 1) return;
+        if (offset.y > bounds.bottom) newOffset.y = bounds.bottom;else if (offset.y < bounds.top) newOffset.y = bounds.top;
 
-    var offset = coordmap.mouseOffset(e);
+        return newOffset;
+      };
+    }
 
-    // Ignore mousedown events outside of plotting region, expanded by
-    // a number of pixels specified in expandPixels.
-    if (opts.brushClip && !coordmap.isInPanel(offset, expandPixels))
-      return;
+    // Add the functions to each panel object.
+    for (var i = 0; i < coordmap.length; i++) {
+      var panel = coordmap[i];
+      addScaleFuns(panel);
+    }
+  };
 
-    brush.up({ x: NaN, y: NaN });
-    brush.down(offset);
+  // This adds functions to the coordmap object to handle various
+  // coordinate-mapping tasks, and send information to the server.
+  // The input coordmap is an array of objects, each of which represents a panel.
+  // coordmap must be an array, even if empty, so that it can be modified in
+  // place; when empty, we add a dummy panel to the array.
+  // It also calls initPanelScales, which modifies each panel object to have
+  // scale, scaleInv, and clip functions.
+  imageutils.initCoordmap = function ($el, coordmap) {
+    var el = $el[0];
+
+    // If we didn't get any panels, create a dummy one where the domain and range
+    // are simply the pixel dimensions.
+    // that we modify.
+    if (coordmap.length === 0) {
+      var bounds = {
+        top: 0,
+        left: 0,
+        right: el.clientWidth - 1,
+        bottom: el.clientHeight - 1
+      };
 
+      coordmap[0] = {
+        domain: bounds,
+        range: bounds,
+        mapping: {}
+      };
+    }
 
-    if (brush.isInResizeArea(offset)) {
-      brush.startResizing(offset);
+    // Add scaling functions to each panel
+    imageutils.initPanelScales(coordmap);
 
-      // Attach the move and up handlers to the window so that they respond
-      // even when the mouse is moved outside of the image.
-      $(document)
-        .on('mousemove.image_brush', mousemoveResizing)
-        .on('mouseup.image_brush', mouseupResizing);
+    // Firefox doesn't have offsetX/Y, so we need to use an alternate
+    // method of calculation for it. Even though other browsers do have
+    // offsetX/Y, we need to calculate relative to $el, because sometimes the
+    // mouse event can come with offset relative to other elements on the
+    // page. This happens when the event listener is bound to, say, window.
+    coordmap.mouseOffset = function (mouseEvent) {
+      var offset = $el.offset();
+      return {
+        x: mouseEvent.pageX - offset.left,
+        y: mouseEvent.pageY - offset.top
+      };
+    };
 
-    } else if (brush.isInsideBrush(offset)) {
-      brush.startDragging(offset);
-      setCursorStyle('grabbing');
+    // Given two sets of x/y coordinates, return an object representing the
+    // min and max x and y values. (This could be generalized to any number
+    // of points).
+    coordmap.findBox = function (offset1, offset2) {
+      return {
+        xmin: Math.min(offset1.x, offset2.x),
+        xmax: Math.max(offset1.x, offset2.x),
+        ymin: Math.min(offset1.y, offset2.y),
+        ymax: Math.max(offset1.y, offset2.y)
+      };
+    };
 
-      // Attach the move and up handlers to the window so that they respond
-      // even when the mouse is moved outside of the image.
-      $(document)
-        .on('mousemove.image_brush', mousemoveDragging)
-        .on('mouseup.image_brush', mouseupDragging);
+    // Shift an array of values so that they are within a min and max.
+    // The vals will be shifted so that they maintain the same spacing
+    // internally. If the range in vals is larger than the range of
+    // min and max, the result might not make sense.
+    coordmap.shiftToRange = function (vals, min, max) {
+      if (!(vals instanceof Array)) vals = [vals];
+
+      var maxval = Math.max.apply(null, vals);
+      var minval = Math.min.apply(null, vals);
+      var shiftAmount = 0;
+      if (maxval > max) {
+        shiftAmount = max - maxval;
+      } else if (minval < min) {
+        shiftAmount = min - minval;
+      }
 
-    } else {
-      var panel = coordmap.getPanel(offset, expandPixels);
-      brush.startBrushing(panel.clip(offset));
-
-      // Attach the move and up handlers to the window so that they respond
-      // even when the mouse is moved outside of the image.
-      $(document)
-        .on('mousemove.image_brush', mousemoveBrushing)
-        .on('mouseup.image_brush', mouseupBrushing);
-    }
-  }
+      var newvals = [];
+      for (var i = 0; i < vals.length; i++) {
+        newvals[i] = vals[i] + shiftAmount;
+      }
+      return newvals;
+    };
 
-  // This sets the cursor style when it's in the el
-  function mousemove(e) {
-    var offset = coordmap.mouseOffset(e);
+    // Given an offset, return an object representing which panel it's in. The
+    // `expand` argument tells it to expand the panel area by that many pixels.
+    // It's possible for an offset to be within more than one panel, because
+    // of the `expand` value. If that's the case, find the nearest panel.
+    coordmap.getPanel = function (offset, expand) {
+      expand = expand || 0;
+
+      var x = offset.x;
+      var y = offset.y;
+
+      var matches = []; // Panels that match
+      var dists = []; // Distance of offset to each matching panel
+      var b;
+      for (var i = 0; i < coordmap.length; i++) {
+        b = coordmap[i].range;
+
+        if (x <= b.right + expand && x >= b.left - expand && y <= b.bottom + expand && y >= b.top - expand) {
+          matches.push(coordmap[i]);
+
+          // Find distance from edges for x and y
+          var xdist = 0;
+          var ydist = 0;
+          if (x > b.right && x <= b.right + expand) {
+            xdist = x - b.right;
+          } else if (x < b.left && x >= b.left - expand) {
+            xdist = x - b.left;
+          }
+          if (y > b.bottom && y <= b.bottom + expand) {
+            ydist = y - b.bottom;
+          } else if (y < b.top && y >= b.top - expand) {
+            ydist = y - b.top;
+          }
 
-    if (!(brush.isBrushing() || brush.isDragging() || brush.isResizing())) {
-      // Set the cursor depending on where it is
-      if (brush.isInResizeArea(offset)) {
-        var r = brush.whichResizeSides(offset);
-
-        if ((r.left && r.top) || (r.right && r.bottom)) {
-          setCursorStyle('nwse-resize');
-        } else if ((r.left && r.bottom) || (r.right && r.top)) {
-          setCursorStyle('nesw-resize');
-        } else if (r.left || r.right) {
-          setCursorStyle('ew-resize');
-        } else if (r.top || r.bottom) {
-          setCursorStyle('ns-resize');
+          // Cartesian distance
+          dists.push(Math.sqrt(Math.pow(xdist, 2) + Math.pow(ydist, 2)));
         }
-      } else if (brush.isInsideBrush(offset)) {
-        setCursorStyle('grabbable');
-      } else if (coordmap.isInPanel(offset, expandPixels)) {
-        setCursorStyle('crosshair');
-      } else {
-        setCursorStyle(null);
       }
-    }
-  }
 
-  // mousemove handlers while brushing or dragging
-  function mousemoveBrushing(e) {
-    brush.brushTo(coordmap.mouseOffset(e));
-    brushInfoSender.normalCall();
-  }
+      if (matches.length) {
+        // Find shortest distance
+        var min_dist = Math.min.apply(null, dists);
+        for (i = 0; i < matches.length; i++) {
+          if (dists[i] === min_dist) {
+            return matches[i];
+          }
+        }
+      }
 
-  function mousemoveDragging(e) {
-    brush.dragTo(coordmap.mouseOffset(e));
-    brushInfoSender.normalCall();
-  }
+      return null;
+    };
 
-  function mousemoveResizing(e) {
-    brush.resizeTo(coordmap.mouseOffset(e));
-    brushInfoSender.normalCall();
-  }
+    // Is an offset in a panel? If supplied, `expand` tells us to expand the
+    // panels by that many pixels in all directions.
+    coordmap.isInPanel = function (offset, expand) {
+      expand = expand || 0;
 
-  // mouseup handlers while brushing or dragging
-  function mouseupBrushing(e) {
-    // Listen for left mouse button only
-    if (e.which !== 1) return;
+      if (coordmap.getPanel(offset, expand)) return true;
 
-    $(document)
-      .off('mousemove.image_brush')
-      .off('mouseup.image_brush');
+      return false;
+    };
 
-    brush.up(coordmap.mouseOffset(e));
+    // Returns a function that sends mouse coordinates, scaled to data space.
+    // If that function is passed a null event, it will send null.
+    coordmap.mouseCoordinateSender = function (inputId, clip, nullOutside) {
+      if (clip === undefined) clip = true;
+      if (nullOutside === undefined) nullOutside = false;
 
-    brush.stopBrushing();
-    setCursorStyle('crosshair');
+      return function (e) {
+        if (e === null) {
+          exports.onInputChange(inputId, null);
+          return;
+        }
 
-    // If the brush didn't go anywhere, hide the brush, clear value,
-    // and return.
-    if (brush.down().x === brush.up().x && brush.down().y === brush.up().y) {
-      brush.reset();
-      brushInfoSender.immediateCall();
-      return;
-    }
+        var offset = coordmap.mouseOffset(e);
+        // If outside of plotting region
+        if (!coordmap.isInPanel(offset)) {
+          if (nullOutside) {
+            exports.onInputChange(inputId, null);
+            return;
+          }
+          if (clip) return;
+        }
+        if (clip && !coordmap.isInPanel(offset)) return;
 
-    // Send info immediately on mouseup, but only if needed. If we don't
-    // do the pending check, we might send the same data twice (with
-    // with difference nonce).
-    if (brushInfoSender.isPending())
-      brushInfoSender.immediateCall();
-  }
+        var panel = coordmap.getPanel(offset);
+        var coords = panel.scaleInv(offset);
 
-  function mouseupDragging(e) {
-    // Listen for left mouse button only
-    if (e.which !== 1) return;
+        // Add the panel (facet) variables, if present
+        $.extend(coords, panel.panel_vars);
 
-    $(document)
-      .off('mousemove.image_brush')
-      .off('mouseup.image_brush');
+        // Add variable name mappings
+        coords.mapping = panel.mapping;
 
-    brush.up(coordmap.mouseOffset(e));
+        // Add scaling information
+        coords.domain = panel.domain;
+        coords.range = panel.range;
+        coords.log = panel.log;
 
-    brush.stopDragging();
-    setCursorStyle('grabbable');
+        coords[".nonce"] = Math.random();
+        exports.onInputChange(inputId, coords);
+      };
+    };
+  };
 
-    if (brushInfoSender.isPending())
-      brushInfoSender.immediateCall();
-  }
+  // This object provides two public event listeners: mousedown, and
+  // dblclickIE8.
+  // We need to make sure that, when the image is listening for double-
+  // clicks, that a double-click doesn't trigger two click events. We'll
+  // trigger custom mousedown2 and dblclick2 events with this mousedown
+  // listener.
+  imageutils.createClickInfo = function ($el, dblclickId, dblclickDelay) {
+    var clickTimer = null;
+    var pending_e = null; // A pending mousedown2 event
+
+    // Create a new event of type eventType (like 'mousedown2'), and trigger
+    // it with the information stored in this.e.
+    function triggerEvent(newEventType, e) {
+      // Extract important info from e and construct a new event with type
+      // eventType.
+      var e2 = $.Event(newEventType, {
+        which: e.which,
+        pageX: e.pageX,
+        pageY: e.pageY,
+        offsetX: e.offsetX,
+        offsetY: e.offsetY
+      });
 
-  function mouseupResizing(e) {
-    // Listen for left mouse button only
-    if (e.which !== 1) return;
+      $el.trigger(e2);
+    }
 
-    $(document)
-      .off('mousemove.image_brush')
-      .off('mouseup.image_brush');
+    function triggerPendingMousedown2() {
+      // It's possible that between the scheduling of a mousedown2 and the
+      // time this callback is executed, someone else triggers a
+      // mousedown2, so check for that.
+      if (pending_e) {
+        triggerEvent('mousedown2', pending_e);
+        pending_e = null;
+      }
+    }
 
-    brush.up(coordmap.mouseOffset(e));
-    brush.stopResizing();
+    // Set a timer to trigger a mousedown2 event, using information from the
+    // last recorded mousdown event.
+    function scheduleMousedown2(e) {
+      pending_e = e;
 
-    if (brushInfoSender.isPending())
-      brushInfoSender.immediateCall();
+      clickTimer = setTimeout(function () {
+        triggerPendingMousedown2();
+      }, dblclickDelay);
+    }
 
-  }
+    function mousedown(e) {
+      // Listen for left mouse button only
+      if (e.which !== 1) return;
 
-  // Brush maintenance: When an image is re-rendered, the brush must either
-  // be removed (if brushResetOnNew) or imported (if !brushResetOnNew). The
-  // "mostRecentBrush" bit is to ensure that when multiple outputs share the
-  // same brush ID, inactive brushes don't send null values up to the server.
+      // If no dblclick listener, immediately trigger a mousedown2 event.
+      if (!dblclickId) {
+        triggerEvent('mousedown2', e);
+        return;
+      }
 
-  // This should be called when the img (not the el) is removed
-  function onRemoveImg() {
-    if (opts.brushResetOnNew) {
-      if ($el.data("mostRecentBrush")) {
-        brush.reset();
-        brushInfoSender.immediateCall();
+      // If there's a dblclick listener, make sure not to count this as a
+      // click on the first mousedown; we need to wait for the dblclick
+      // delay before we can be sure this click was a single-click.
+      if (pending_e === null) {
+        scheduleMousedown2(e);
+      } else {
+        clearTimeout(clickTimer);
+
+        // If second click is too far away, it doesn't count as a double
+        // click. Instead, immediately trigger a mousedown2 for the previous
+        // click, and set this click as a new first click.
+        if (pending_e && Math.abs(pending_e.offsetX - e.offsetX) > 2 || Math.abs(pending_e.offsetY - e.offsetY) > 2) {
+
+          triggerPendingMousedown2();
+          scheduleMousedown2(e);
+        } else {
+          // The second click was close to the first one. If it happened
+          // within specified delay, trigger our custom 'dblclick2' event.
+          pending_e = null;
+          triggerEvent('dblclick2', e);
+        }
       }
     }
-  }
 
-  if (!opts.brushResetOnNew) {
-    if ($el.data("mostRecentBrush")) {
-      brush.importOldBrush();
-      brushInfoSender.immediateCall();
+    // IE8 needs a special hack because when you do a double-click it doesn't
+    // trigger the click event twice - it directly triggers dblclick.
+    function dblclickIE8(e) {
+      e.which = 1; // In IE8, e.which is 0 instead of 1. ???
+      triggerEvent('dblclick2', e);
     }
-  }
 
-  return {
-    mousedown: mousedown,
-    mousemove: mousemove,
-    onRemoveImg: onRemoveImg
-  };
-};
-
-// Returns an object that represents the state of the brush. This gets wrapped
-// in a brushHandler, which provides various event listeners.
-imageutils.createBrush = function($el, opts, coordmap, expandPixels) {
-  // Number of pixels outside of brush to allow start resizing
-  var resizeExpand = 10;
-
-  var el = $el[0];
-  var $div = null;  // The div representing the brush
-
-  var state = {};
-  reset();
-
-  function reset() {
-    // Current brushing/dragging/resizing state
-    state.brushing = false;
-    state.dragging = false;
-    state.resizing = false;
-
-    // Offset of last mouse down and up events
-    state.down = { x: NaN, y: NaN };
-    state.up   = { x: NaN, y: NaN };
-
-    // Which side(s) we're currently resizing
-    state.resizeSides = {
-      left: false,
-      right: false,
-      top: false,
-      bottom: false
+    return {
+      mousedown: mousedown,
+      dblclickIE8: dblclickIE8
     };
+  };
 
-    // Bounding rectangle of the brush, in pixel and data dimensions. We need to
-    // record data dimensions along with pixel dimensions so that when a new
-    // plot is sent, we can re-draw the brush div with the appropriate coords.
-    state.boundsPx = {
-      xmin: NaN,
-      xmax: NaN,
-      ymin: NaN,
-      ymax: NaN
-    };
-    state.boundsData = {
-      xmin: NaN,
-      xmax: NaN,
-      ymin: NaN,
-      ymax: NaN
-    };
+  // ----------------------------------------------------------
+  // Handler creators for click, hover, brush.
+  // Each of these returns an object with a few public members. These public
+  // members are callbacks that are meant to be bound to events on $el with
+  // the same name (like 'mousedown').
+  // ----------------------------------------------------------
 
-    // Panel object that the brush is in
-    state.panel = null;
+  imageutils.createClickHandler = function (inputId, clip, coordmap) {
+    var clickInfoSender = coordmap.mouseCoordinateSender(inputId, clip);
 
-    // The bounds at the start of a drag/resize
-    state.changeStartBounds = {
-      xmin: NaN,
-      xmax: NaN,
-      ymin: NaN,
-      ymax: NaN
+    return {
+      mousedown: function mousedown(e) {
+        // Listen for left mouse button only
+        if (e.which !== 1) return;
+        clickInfoSender(e);
+      },
+      onResetImg: function onResetImg() {
+        clickInfoSender(null);
+      }
     };
+  };
 
-    if ($div)
-      $div.remove();
-  }
-
-  // If there's an existing brush div, use that div to set the new brush's
-  // settings, provided that the x, y, and panel variables have the same names,
-  // and there's a panel with matching panel variable values.
-  function importOldBrush() {
-    var oldDiv = $el.find('#' + el.id + '_brush');
-    if (oldDiv.length === 0)
-      return;
+  imageutils.createHoverHandler = function (inputId, delay, delayType, clip, nullOutside, coordmap) {
+    var sendHoverInfo = coordmap.mouseCoordinateSender(inputId, clip, nullOutside);
 
-    var oldBoundsData = oldDiv.data('bounds-data');
-    var oldPanel = oldDiv.data('panel');
+    var hoverInfoSender;
+    if (delayType === 'throttle') hoverInfoSender = new Throttler(null, sendHoverInfo, delay);else hoverInfoSender = new Debouncer(null, sendHoverInfo, delay);
 
-    if (!oldBoundsData || !oldPanel)
-      return;
+    // What to do when mouse exits the image
+    var mouseout;
+    if (nullOutside) mouseout = function mouseout() {
+      hoverInfoSender.normalCall(null);
+    };else mouseout = function mouseout() {};
 
-    // Compare two objects. This checks that objects a and b have the same est
-    // of keys, and that each key has the same value. This function isn't
-    // perfect, but it's good enough for comparing variable mappings, below.
-    function isEquivalent(a, b) {
-      if (a === undefined) {
-        if (b === undefined)
-          return true;
-        else
-          return false;
-      }
-      if (a === null) {
-        if (b === null)
-          return true;
-        else
-          return false;
+    return {
+      mousemove: function mousemove(e) {
+        hoverInfoSender.normalCall(e);
+      },
+      mouseout: mouseout,
+      onResetImg: function onResetImg() {
+        hoverInfoSender.immediateCall(null);
       }
+    };
+  };
 
-      var aProps = Object.getOwnPropertyNames(a);
-      var bProps = Object.getOwnPropertyNames(b);
+  // Returns a brush handler object. This has three public functions:
+  // mousedown, mousemove, and onResetImg.
+  imageutils.createBrushHandler = function (inputId, $el, opts, coordmap, outputId) {
+    // Parameter: expand the area in which a brush can be started, by this
+    // many pixels in all directions. (This should probably be a brush option)
+    var expandPixels = 20;
+
+    // Represents the state of the brush
+    var brush = imageutils.createBrush($el, opts, coordmap, expandPixels);
+
+    // Brush IDs can span multiple image/plot outputs. When an output is brushed,
+    // if a brush with the same ID is active on a different image/plot, it must
+    // be dismissed (but without sending any data to the server). We implement
+    // this by sending the shiny-internal:brushed event to all plots, and letting
+    // each plot decide for itself what to do.
+    //
+    // The decision to have the event sent to each plot (as opposed to a single
+    // event triggered on, say, the document) was made to make cleanup easier;
+    // listening on an event on the document would prevent garbage collection
+    // of plot outputs that are removed from the document.
+    $el.on("shiny-internal:brushed.image_output", function (e, coords) {
+      // If the new brush shares our ID but not our output element ID, we
+      // need to clear our brush (if any).
+      if (coords.brushId === inputId && coords.outputId !== outputId) {
+        $el.data("mostRecentBrush", false);
+        brush.reset();
+      }
+    });
 
-      if (aProps.length != bProps.length)
-        return false;
+    // Set cursor to one of 7 styles. We need to set the cursor on the whole
+    // el instead of the brush div, because the brush div has
+    // 'pointer-events:none' so that it won't intercept pointer events.
+    // If `style` is null, don't add a cursor style.
+    function setCursorStyle(style) {
+      $el.removeClass('crosshair grabbable grabbing ns-resize ew-resize nesw-resize nwse-resize');
 
-      for (var i=0; i<aProps.length; i++) {
-        var propName = aProps[i];
-        if (a[propName] !== b[propName]) {
-          return false;
-        }
-      }
-      return true;
+      if (style) $el.addClass(style);
     }
 
-    // Find a panel that has matching vars; if none found, we can't restore.
-    // The oldPanel and new panel must match on their mapping vars, and the
-    // values.
-    for (var i=0; i<coordmap.length; i++){
-      var curPanel = coordmap[i];
+    function sendBrushInfo() {
+      var coords = brush.boundsData();
 
-      if (isEquivalent(oldPanel.mapping, curPanel.mapping) &&
-          isEquivalent(oldPanel.panel_vars, curPanel.panel_vars)) {
-        // We've found a matching panel
-        state.panel = coordmap[i];
-        break;
+      // We're in a new or reset state
+      if (isNaN(coords.xmin)) {
+        exports.onInputChange(inputId, null);
+        // Must tell other brushes to clear.
+        imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
+          brushId: inputId, outputId: null
+        });
+        return;
       }
-    }
 
-    // If we didn't find a matching panel, remove the old div and return
-    if (state.panel === null) {
-      oldDiv.remove();
-      return;
-    }
+      var panel = brush.getPanel();
 
-    $div = oldDiv;
+      // Add the panel (facet) variables, if present
+      $.extend(coords, panel.panel_vars);
 
-    boundsData(oldBoundsData);
-    updateDiv();
-  }
+      // Add variable name mappings
+      coords.mapping = panel.mapping;
 
-  // Return true if the offset is inside min/max coords
-  function isInsideBrush(offset) {
-    var bounds = state.boundsPx;
-    return offset.x <= bounds.xmax && offset.x >= bounds.xmin &&
-           offset.y <= bounds.ymax && offset.y >= bounds.ymin;
-  }
+      // Add scaling information
+      coords.domain = panel.domain;
+      coords.range = panel.range;
+      coords.log = panel.log;
 
-  // Return true if offset is inside a region to start a resize
-  function isInResizeArea(offset) {
-    var sides = whichResizeSides(offset);
-    return sides.left || sides.right || sides.top || sides.bottom;
-  }
+      coords.direction = opts.brushDirection;
 
-  // Return an object representing which resize region(s) the cursor is in.
-  function whichResizeSides(offset) {
-    var b = state.boundsPx;
-    // Bounds with expansion
-    var e = {
-      xmin: b.xmin - resizeExpand,
-      xmax: b.xmax + resizeExpand,
-      ymin: b.ymin - resizeExpand,
-      ymax: b.ymax + resizeExpand
-    };
-    var res = {
-      left: false,
-      right: false,
-      top: false,
-      bottom: false
-    };
+      coords.brushId = inputId;
+      coords.outputId = outputId;
+
+      // Send data to server
+      exports.onInputChange(inputId, coords);
 
-    if ((opts.brushDirection === 'xy' || opts.brushDirection === 'x') &&
-        (offset.y <= e.ymax && offset.y >= e.ymin))
-    {
-      if (offset.x < b.xmin && offset.x >= e.xmin)
-        res.left = true;
-      else if (offset.x > b.xmax && offset.x <= e.xmax)
-        res.right = true;
+      $el.data("mostRecentBrush", true);
+      imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
     }
 
-    if ((opts.brushDirection === 'xy' || opts.brushDirection === 'y') &&
-        (offset.x <= e.xmax && offset.x >= e.xmin))
-    {
-      if (offset.y < b.ymin && offset.y >= e.ymin)
-        res.top = true;
-      else if (offset.y > b.ymax && offset.y <= e.ymax)
-        res.bottom = true;
+    var brushInfoSender;
+    if (opts.brushDelayType === 'throttle') {
+      brushInfoSender = new Throttler(null, sendBrushInfo, opts.brushDelay);
+    } else {
+      brushInfoSender = new Debouncer(null, sendBrushInfo, opts.brushDelay);
     }
 
-    return res;
-  }
+    function mousedown(e) {
+      // This can happen when mousedown inside the graphic, then mouseup
+      // outside, then mousedown inside. Just ignore the second
+      // mousedown.
+      if (brush.isBrushing() || brush.isDragging() || brush.isResizing()) return;
 
+      // Listen for left mouse button only
+      if (e.which !== 1) return;
 
-  // Sets the bounds of the brush, given a box and optional panel. This
-  // will fit the box bounds into the panel, so we don't brush outside of it.
-  // This knows whether we're brushing in the x, y, or xy directions, and sets
-  // bounds accordingly.
-  // If no box is passed in, return current bounds.
-  function boundsPx(box) {
-    if (box === undefined)
-      return state.boundsPx;
+      var offset = coordmap.mouseOffset(e);
 
-    var min = { x: box.xmin, y: box.ymin };
-    var max = { x: box.xmax, y: box.ymax };
+      // Ignore mousedown events outside of plotting region, expanded by
+      // a number of pixels specified in expandPixels.
+      if (opts.brushClip && !coordmap.isInPanel(offset, expandPixels)) return;
 
-    var panel = state.panel;
-    var panelBounds = panel.range;
+      brush.up({ x: NaN, y: NaN });
+      brush.down(offset);
 
-    if (opts.brushClip) {
-      min = panel.clip(min);
-      max = panel.clip(max);
-    }
+      if (brush.isInResizeArea(offset)) {
+        brush.startResizing(offset);
 
-    if (opts.brushDirection === 'xy') {
-      // No change
+        // Attach the move and up handlers to the window so that they respond
+        // even when the mouse is moved outside of the image.
+        $(document).on('mousemove.image_brush', mousemoveResizing).on('mouseup.image_brush', mouseupResizing);
+      } else if (brush.isInsideBrush(offset)) {
+        brush.startDragging(offset);
+        setCursorStyle('grabbing');
 
-    } else if (opts.brushDirection === 'x') {
-      // Extend top and bottom of plotting area
-      min.y = panelBounds.top;
-      max.y = panelBounds.bottom;
+        // Attach the move and up handlers to the window so that they respond
+        // even when the mouse is moved outside of the image.
+        $(document).on('mousemove.image_brush', mousemoveDragging).on('mouseup.image_brush', mouseupDragging);
+      } else {
+        var panel = coordmap.getPanel(offset, expandPixels);
+        brush.startBrushing(panel.clip(offset));
 
-    } else if (opts.brushDirection === 'y') {
-      min.x = panelBounds.left;
-      max.x = panelBounds.right;
+        // Attach the move and up handlers to the window so that they respond
+        // even when the mouse is moved outside of the image.
+        $(document).on('mousemove.image_brush', mousemoveBrushing).on('mouseup.image_brush', mouseupBrushing);
+      }
     }
 
-    state.boundsPx = {
-      xmin: min.x,
-      xmax: max.x,
-      ymin: min.y,
-      ymax: max.y
-    };
+    // This sets the cursor style when it's in the el
+    function mousemove(e) {
+      var offset = coordmap.mouseOffset(e);
 
-    // Positions in data space
-    var minData = state.panel.scaleInv(min);
-    var maxData = state.panel.scaleInv(max);
-    // For reversed scales, the min and max can be reversed, so use findBox
-    // to ensure correct order.
-    state.boundsData = coordmap.findBox(minData, maxData);
-
-    // We also need to attach the data bounds and panel as data attributes, so
-    // that if the image is re-sent, we can grab the data bounds to create a new
-    // brush. This should be fast because it doesn't actually modify the DOM.
-    $div.data('bounds-data', state.boundsData);
-    $div.data('panel', state.panel);
-  }
+      if (!(brush.isBrushing() || brush.isDragging() || brush.isResizing())) {
+        // Set the cursor depending on where it is
+        if (brush.isInResizeArea(offset)) {
+          var r = brush.whichResizeSides(offset);
+
+          if (r.left && r.top || r.right && r.bottom) {
+            setCursorStyle('nwse-resize');
+          } else if (r.left && r.bottom || r.right && r.top) {
+            setCursorStyle('nesw-resize');
+          } else if (r.left || r.right) {
+            setCursorStyle('ew-resize');
+          } else if (r.top || r.bottom) {
+            setCursorStyle('ns-resize');
+          }
+        } else if (brush.isInsideBrush(offset)) {
+          setCursorStyle('grabbable');
+        } else if (coordmap.isInPanel(offset, expandPixels)) {
+          setCursorStyle('crosshair');
+        } else {
+          setCursorStyle(null);
+        }
+      }
+    }
 
-  // Get or set the bounds of the brush using coordinates in the data space.
-  function boundsData(box) {
-    if (box === undefined) {
-      return state.boundsData;
+    // mousemove handlers while brushing or dragging
+    function mousemoveBrushing(e) {
+      brush.brushTo(coordmap.mouseOffset(e));
+      brushInfoSender.normalCall();
     }
 
-    var min = { x: box.xmin, y: box.ymin };
-    var max = { x: box.xmax, y: box.ymax };
+    function mousemoveDragging(e) {
+      brush.dragTo(coordmap.mouseOffset(e));
+      brushInfoSender.normalCall();
+    }
 
-    var minPx = state.panel.scale(min);
-    var maxPx = state.panel.scale(max);
+    function mousemoveResizing(e) {
+      brush.resizeTo(coordmap.mouseOffset(e));
+      brushInfoSender.normalCall();
+    }
 
-    // The scaling function can reverse the direction of the axes, so we need to
-    // find the min and max again.
-    boundsPx({
-      xmin: Math.min(minPx.x, maxPx.x),
-      xmax: Math.max(minPx.x, maxPx.x),
-      ymin: Math.min(minPx.y, maxPx.y),
-      ymax: Math.max(minPx.y, maxPx.y)
-    });
-  }
+    // mouseup handlers while brushing or dragging
+    function mouseupBrushing(e) {
+      // Listen for left mouse button only
+      if (e.which !== 1) return;
 
-  function getPanel() {
-    return state.panel;
-  }
+      $(document).off('mousemove.image_brush').off('mouseup.image_brush');
 
-  // Add a new div representing the brush.
-  function addDiv() {
-    if ($div) $div.remove();
+      brush.up(coordmap.mouseOffset(e));
 
-    // Start hidden; we'll show it when movement occurs
-    $div = $(document.createElement('div'))
-      .attr('id', el.id + '_brush')
-      .css({
-        'background-color': opts.brushFill,
-        'opacity': opts.brushOpacity,
-        'pointer-events': 'none',
-        'position': 'absolute'
-      })
-      .hide();
+      brush.stopBrushing();
+      setCursorStyle('crosshair');
 
-    var borderStyle = '1px solid ' + opts.brushStroke;
-    if (opts.brushDirection === 'xy') {
-      $div.css({
-        'border': borderStyle
-      });
-    } else if (opts.brushDirection === 'x') {
-      $div.css({
-        'border-left': borderStyle,
-        'border-right': borderStyle
-      });
-    } else if (opts.brushDirection === 'y') {
-      $div.css({
-        'border-top': borderStyle,
-        'border-bottom': borderStyle
-      });
+      // If the brush didn't go anywhere, hide the brush, clear value,
+      // and return.
+      if (brush.down().x === brush.up().x && brush.down().y === brush.up().y) {
+        brush.reset();
+        brushInfoSender.immediateCall();
+        return;
+      }
+
+      // Send info immediately on mouseup, but only if needed. If we don't
+      // do the pending check, we might send the same data twice (with
+      // with difference nonce).
+      if (brushInfoSender.isPending()) brushInfoSender.immediateCall();
     }
 
-    $el.append($div);
-    $div.offset({x:0, y:0}).width(0).outerHeight(0);
-  }
+    function mouseupDragging(e) {
+      // Listen for left mouse button only
+      if (e.which !== 1) return;
 
-  // Update the brush div to reflect the current brush bounds.
-  function updateDiv() {
-    // Need parent offset relative to page to calculate mouse offset
-    // relative to page.
-    var imgOffset = $el.offset();
-    var b = state.boundsPx;
-    $div.offset({
-        top: imgOffset.top + b.ymin,
-        left: imgOffset.left + b.xmin
-      })
-      .outerWidth(b.xmax - b.xmin + 1)
-      .outerHeight(b.ymax - b.ymin + 1);
-  }
+      $(document).off('mousemove.image_brush').off('mouseup.image_brush');
 
-  function down(offset) {
-    if (offset === undefined)
-      return state.down;
+      brush.up(coordmap.mouseOffset(e));
 
-    state.down = offset;
-  }
+      brush.stopDragging();
+      setCursorStyle('grabbable');
 
-  function up(offset) {
-    if (offset === undefined)
-      return state.up;
+      if (brushInfoSender.isPending()) brushInfoSender.immediateCall();
+    }
 
-    state.up = offset;
-  }
-
-  function isBrushing() {
-    return state.brushing;
-  }
-
-  function startBrushing() {
-    state.brushing = true;
-    addDiv();
-    state.panel = coordmap.getPanel(state.down, expandPixels);
+    function mouseupResizing(e) {
+      // Listen for left mouse button only
+      if (e.which !== 1) return;
 
-    boundsPx(coordmap.findBox(state.down, state.down));
-    updateDiv();
-  }
+      $(document).off('mousemove.image_brush').off('mouseup.image_brush');
 
-  function brushTo(offset) {
-    boundsPx(coordmap.findBox(state.down, offset));
-    $div.show();
-    updateDiv();
-  }
+      brush.up(coordmap.mouseOffset(e));
+      brush.stopResizing();
 
-  function stopBrushing() {
-    state.brushing = false;
+      if (brushInfoSender.isPending()) brushInfoSender.immediateCall();
+    }
 
-    // Save the final bounding box of the brush
-    boundsPx(coordmap.findBox(state.down, state.up));
-  }
+    // Brush maintenance: When an image is re-rendered, the brush must either
+    // be removed (if brushResetOnNew) or imported (if !brushResetOnNew). The
+    // "mostRecentBrush" bit is to ensure that when multiple outputs share the
+    // same brush ID, inactive brushes don't send null values up to the server.
 
-  function isDragging() {
-    return state.dragging;
-  }
+    // This should be called when the img (not the el) is reset
+    function onResetImg() {
+      if (opts.brushResetOnNew) {
+        if ($el.data("mostRecentBrush")) {
+          brush.reset();
+          brushInfoSender.immediateCall();
+        }
+      }
+    }
 
-  function startDragging() {
-    state.dragging = true;
-    state.changeStartBounds = $.extend({}, state.boundsPx);
-  }
+    if (!opts.brushResetOnNew) {
+      if ($el.data("mostRecentBrush")) {
+        brush.importOldBrush();
+        brushInfoSender.immediateCall();
+      }
+    }
 
-  function dragTo(offset) {
-    // How far the brush was dragged
-    var dx = offset.x - state.down.x;
-    var dy = offset.y - state.down.y;
-
-    // Calculate what new positions would be, before clipping.
-    var start = state.changeStartBounds;
-    var newBounds = {
-      xmin: start.xmin + dx,
-      xmax: start.xmax + dx,
-      ymin: start.ymin + dy,
-      ymax: start.ymax + dy
+    return {
+      mousedown: mousedown,
+      mousemove: mousemove,
+      onResetImg: onResetImg
     };
+  };
 
-    // Clip to the plotting area
-    if (opts.brushClip) {
-      var panelBounds = state.panel.range;
+  // Returns an object that represents the state of the brush. This gets wrapped
+  // in a brushHandler, which provides various event listeners.
+  imageutils.createBrush = function ($el, opts, coordmap, expandPixels) {
+    // Number of pixels outside of brush to allow start resizing
+    var resizeExpand = 10;
+
+    var el = $el[0];
+    var $div = null; // The div representing the brush
+
+    var state = {};
+    reset();
+
+    function reset() {
+      // Current brushing/dragging/resizing state
+      state.brushing = false;
+      state.dragging = false;
+      state.resizing = false;
+
+      // Offset of last mouse down and up events
+      state.down = { x: NaN, y: NaN };
+      state.up = { x: NaN, y: NaN };
+
+      // Which side(s) we're currently resizing
+      state.resizeSides = {
+        left: false,
+        right: false,
+        top: false,
+        bottom: false
+      };
 
-      // Convert to format for shiftToRange
-      var xvals = [ newBounds.xmin, newBounds.xmax ];
-      var yvals = [ newBounds.ymin, newBounds.ymax ];
+      // Bounding rectangle of the brush, in pixel and data dimensions. We need to
+      // record data dimensions along with pixel dimensions so that when a new
+      // plot is sent, we can re-draw the brush div with the appropriate coords.
+      state.boundsPx = {
+        xmin: NaN,
+        xmax: NaN,
+        ymin: NaN,
+        ymax: NaN
+      };
+      state.boundsData = {
+        xmin: NaN,
+        xmax: NaN,
+        ymin: NaN,
+        ymax: NaN
+      };
 
-      xvals = coordmap.shiftToRange(xvals, panelBounds.left, panelBounds.right);
-      yvals = coordmap.shiftToRange(yvals, panelBounds.top,  panelBounds.bottom);
+      // Panel object that the brush is in
+      state.panel = null;
 
-      // Convert back to bounds format
-      newBounds = {
-        xmin: xvals[0],
-        xmax: xvals[1],
-        ymin: yvals[0],
-        ymax: yvals[1]
+      // The bounds at the start of a drag/resize
+      state.changeStartBounds = {
+        xmin: NaN,
+        xmax: NaN,
+        ymin: NaN,
+        ymax: NaN
       };
-    }
 
-    boundsPx(newBounds);
-    updateDiv();
-  }
+      if ($div) $div.remove();
+    }
 
-  function stopDragging() {
-    state.dragging = false;
-  }
+    // If there's an existing brush div, use that div to set the new brush's
+    // settings, provided that the x, y, and panel variables have the same names,
+    // and there's a panel with matching panel variable values.
+    function importOldBrush() {
+      var oldDiv = $el.find('#' + el.id + '_brush');
+      if (oldDiv.length === 0) return;
 
-  function isResizing() {
-    return state.resizing;
-  }
+      var oldBoundsData = oldDiv.data('bounds-data');
+      var oldPanel = oldDiv.data('panel');
 
-  function startResizing() {
-    state.resizing = true;
-    state.changeStartBounds = $.extend({}, state.boundsPx);
-    state.resizeSides = whichResizeSides(state.down);
-  }
+      if (!oldBoundsData || !oldPanel) return;
 
-  function resizeTo(offset) {
-    // How far the brush was dragged
-    var dx = offset.x - state.down.x;
-    var dy = offset.y - state.down.y;
+      // Compare two objects. This checks that objects a and b have the same est
+      // of keys, and that each key has the same value. This function isn't
+      // perfect, but it's good enough for comparing variable mappings, below.
+      function isEquivalent(a, b) {
+        if (a === undefined) {
+          if (b === undefined) return true;else return false;
+        }
+        if (a === null) {
+          if (b === null) return true;else return false;
+        }
 
-    // Calculate what new positions would be, before clipping.
-    var b = $.extend({}, state.changeStartBounds);
-    var panelBounds = state.panel.range;
+        var aProps = Object.getOwnPropertyNames(a);
+        var bProps = Object.getOwnPropertyNames(b);
 
-    if (state.resizeSides.left) {
-      b.xmin = coordmap.shiftToRange([b.xmin + dx], panelBounds.left, b.xmax)[0];
-    } else if (state.resizeSides.right) {
-      b.xmax = coordmap.shiftToRange([b.xmax + dx], b.xmin, panelBounds.right)[0];
-    }
+        if (aProps.length !== bProps.length) return false;
 
-    if (state.resizeSides.top) {
-      b.ymin = coordmap.shiftToRange([b.ymin + dy], panelBounds.top, b.ymax)[0];
-    } else if (state.resizeSides.bottom) {
-      b.ymax = coordmap.shiftToRange([b.ymax + dy], b.ymin, panelBounds.bottom)[0];
-    }
+        for (var i = 0; i < aProps.length; i++) {
+          var propName = aProps[i];
+          if (a[propName] !== b[propName]) {
+            return false;
+          }
+        }
+        return true;
+      }
 
-    boundsPx(b);
-    updateDiv();
-  }
+      // Find a panel that has matching vars; if none found, we can't restore.
+      // The oldPanel and new panel must match on their mapping vars, and the
+      // values.
+      for (var i = 0; i < coordmap.length; i++) {
+        var curPanel = coordmap[i];
 
-  function stopResizing() {
-    state.resizing = false;
-  }
+        if (isEquivalent(oldPanel.mapping, curPanel.mapping) && isEquivalent(oldPanel.panel_vars, curPanel.panel_vars)) {
+          // We've found a matching panel
+          state.panel = coordmap[i];
+          break;
+        }
+      }
 
-  return {
-    reset: reset,
+      // If we didn't find a matching panel, remove the old div and return
+      if (state.panel === null) {
+        oldDiv.remove();
+        return;
+      }
 
-    importOldBrush: importOldBrush,
-    isInsideBrush: isInsideBrush,
-    isInResizeArea: isInResizeArea,
-    whichResizeSides: whichResizeSides,
+      $div = oldDiv;
 
-    boundsPx: boundsPx,
-    boundsData: boundsData,
-    getPanel: getPanel,
+      boundsData(oldBoundsData);
+      updateDiv();
+    }
 
-    down: down,
-    up: up,
+    // Return true if the offset is inside min/max coords
+    function isInsideBrush(offset) {
+      var bounds = state.boundsPx;
+      return offset.x <= bounds.xmax && offset.x >= bounds.xmin && offset.y <= bounds.ymax && offset.y >= bounds.ymin;
+    }
 
-    isBrushing: isBrushing,
-    startBrushing: startBrushing,
-    brushTo: brushTo,
-    stopBrushing: stopBrushing,
+    // Return true if offset is inside a region to start a resize
+    function isInResizeArea(offset) {
+      var sides = whichResizeSides(offset);
+      return sides.left || sides.right || sides.top || sides.bottom;
+    }
 
-    isDragging: isDragging,
-    startDragging: startDragging,
-    dragTo: dragTo,
-    stopDragging: stopDragging,
+    // Return an object representing which resize region(s) the cursor is in.
+    function whichResizeSides(offset) {
+      var b = state.boundsPx;
+      // Bounds with expansion
+      var e = {
+        xmin: b.xmin - resizeExpand,
+        xmax: b.xmax + resizeExpand,
+        ymin: b.ymin - resizeExpand,
+        ymax: b.ymax + resizeExpand
+      };
+      var res = {
+        left: false,
+        right: false,
+        top: false,
+        bottom: false
+      };
 
-    isResizing: isResizing,
-    startResizing: startResizing,
-    resizeTo: resizeTo,
-    stopResizing: stopResizing
-  };
-};
+      if ((opts.brushDirection === 'xy' || opts.brushDirection === 'x') && offset.y <= e.ymax && offset.y >= e.ymin) {
+        if (offset.x < b.xmin && offset.x >= e.xmin) res.left = true;else if (offset.x > b.xmax && offset.x <= e.xmax) res.right = true;
+      }
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/output_binding_html.js
-
-var htmlOutputBinding = new OutputBinding();
-$.extend(htmlOutputBinding, {
-  find: function(scope) {
-    return $(scope).find('.shiny-html-output');
-  },
-  onValueError: function(el, err) {
-    exports.unbindAll(el);
-    this.renderError(el, err);
-  },
-  renderValue: function(el, data) {
-    exports.unbindAll(el);
+      if ((opts.brushDirection === 'xy' || opts.brushDirection === 'y') && offset.x <= e.xmax && offset.x >= e.xmin) {
+        if (offset.y < b.ymin && offset.y >= e.ymin) res.top = true;else if (offset.y > b.ymax && offset.y <= e.ymax) res.bottom = true;
+      }
 
-    var html;
-    var dependencies = [];
-    if (data === null) {
-      html = '';
-    } else if (typeof(data) === 'string') {
-      html = data;
-    } else if (typeof(data) === 'object') {
-      html = data.html;
-      dependencies = data.deps;
+      return res;
     }
 
-    exports.renderHtml(html, el, dependencies);
-    exports.initializeInputs(el);
-    exports.bindAll(el);
-  }
-});
-outputBindings.register(htmlOutputBinding, 'shiny.htmlOutput');
+    // Sets the bounds of the brush, given a box and optional panel. This
+    // will fit the box bounds into the panel, so we don't brush outside of it.
+    // This knows whether we're brushing in the x, y, or xy directions, and sets
+    // bounds accordingly.
+    // If no box is passed in, just return current bounds.
+    function boundsPx(box) {
+      if (box === undefined) return state.boundsPx;
 
-var renderDependencies = exports.renderDependencies = function(dependencies) {
-  if (dependencies) {
-    $.each(dependencies, function(i, dep) {
-      renderDependency(dep);
-    });
-  }
-};
+      var min = { x: box.xmin, y: box.ymin };
+      var max = { x: box.xmax, y: box.ymax };
 
-// Render HTML in a DOM element, inserting singletons into head as needed
-exports.renderHtml = function(html, el, dependencies) {
-  renderDependencies(dependencies);
-  return singletons.renderHtml(html, el);
-};
+      var panel = state.panel;
+      var panelBounds = panel.range;
 
-var htmlDependencies = {};
-function registerDependency(name, version) {
-  htmlDependencies[name] = version;
-}
+      if (opts.brushClip) {
+        min = panel.clip(min);
+        max = panel.clip(max);
+      }
 
-// Client-side dependency resolution and rendering
-function renderDependency(dep) {
-  if (htmlDependencies.hasOwnProperty(dep.name))
-    return false;
+      if (opts.brushDirection === 'xy') {
+        // No change
 
-  registerDependency(dep.name, dep.version);
+      } else if (opts.brushDirection === 'x') {
+        // Extend top and bottom of plotting area
+        min.y = panelBounds.top;
+        max.y = panelBounds.bottom;
+      } else if (opts.brushDirection === 'y') {
+        min.x = panelBounds.left;
+        max.x = panelBounds.right;
+      }
 
-  var href = dep.src.href;
+      state.boundsPx = {
+        xmin: min.x,
+        xmax: max.x,
+        ymin: min.y,
+        ymax: max.y
+      };
 
-  var $head = $("head").first();
+      // Positions in data space
+      var minData = state.panel.scaleInv(min);
+      var maxData = state.panel.scaleInv(max);
+      // For reversed scales, the min and max can be reversed, so use findBox
+      // to ensure correct order.
+      state.boundsData = coordmap.findBox(minData, maxData);
+
+      // We also need to attach the data bounds and panel as data attributes, so
+      // that if the image is re-sent, we can grab the data bounds to create a new
+      // brush. This should be fast because it doesn't actually modify the DOM.
+      $div.data('bounds-data', state.boundsData);
+      $div.data('panel', state.panel);
+      return undefined;
+    }
 
-  if (dep.meta) {
-    var metas = $.map(asArray(dep.meta), function(content, name) {
-      return $("<meta>").attr("name", name).attr("content", content);
-    });
-    $head.append(metas);
-  }
+    // Get or set the bounds of the brush using coordinates in the data space.
+    function boundsData(box) {
+      if (box === undefined) {
+        return state.boundsData;
+      }
 
-  if (dep.stylesheet) {
-    var stylesheets = $.map(asArray(dep.stylesheet), function(stylesheet) {
-      return $("<link rel='stylesheet' type='text/css'>")
-        .attr("href", href + "/" + encodeURI(stylesheet));
-    });
-    $head.append(stylesheets);
-  }
+      var min = { x: box.xmin, y: box.ymin };
+      var max = { x: box.xmax, y: box.ymax };
 
-  if (dep.script) {
-    var scripts = $.map(asArray(dep.script), function(scriptName) {
-      return $("<script>").attr("src", href + "/" + encodeURI(scriptName));
-    });
-    $head.append(scripts);
-  }
+      var minPx = state.panel.scale(min);
+      var maxPx = state.panel.scale(max);
 
-  if (dep.attachment) {
-    // dep.attachment might be a single string, an array, or an object.
-    var attachments = dep.attachment;
-    if (typeof(attachments) === "string")
-      attachments = [attachments];
-    if ($.isArray(attachments)) {
-      // The contract for attachments is that arrays of attachments are
-      // addressed using 1-based indexes. Convert this array to an object.
-      var tmp = {};
-      $.each(attachments, function(index, attachment) {
-        tmp[(index + 1) + ""] = attachment;
+      // The scaling function can reverse the direction of the axes, so we need to
+      // find the min and max again.
+      boundsPx({
+        xmin: Math.min(minPx.x, maxPx.x),
+        xmax: Math.max(minPx.x, maxPx.x),
+        ymin: Math.min(minPx.y, maxPx.y),
+        ymax: Math.max(minPx.y, maxPx.y)
       });
-      attachments = tmp;
+      return undefined;
     }
 
-    var attach = $.map(attachments, function(attachment, key) {
-      return $("<link rel='attachment'>")
-        .attr("id", dep.name + "-" + key + "-attachment")
-        .attr("href", href + "/" + encodeURI(attachment));
-    });
-    $head.append(attach);
-  }
-
-  if (dep.head) {
-    var $newHead = $("<head></head>");
-    $newHead.html(dep.head);
-    $head.append($newHead.children());
-  }
-  return true;
-}
-
-var singletons = {
-  knownSingletons: {},
-  renderHtml: function(html, el) {
-    var processed = this._processHtml(html);
-    this._addToHead(processed.head);
-    this.register(processed.singletons);
-    $(el).html(processed.html);
-    return processed;
-  },
-  // Take an object where keys are names of singletons, and merges it into
-  // knownSingletons
-  register: function(s) {
-    $.extend(this.knownSingletons, s);
-  },
-  // Takes a string or array of strings and adds them to knownSingletons
-  registerNames: function(s) {
-    if (typeof s === 'string') {
-      this.knownSingletons[s] = true;
-    } else if (s instanceof Array) {
-      for (var i = 0; i < s.length; i++) {
-        this.knownSingletons[s[i]] = true;
-      }
+    function getPanel() {
+      return state.panel;
     }
-  },
-  // Inserts new content into document head
-  _addToHead: function(head) {
-    if (head.length > 0) {
-      var tempDiv = $("<div>" + head + "</div>")[0];
-      var $head = $('head');
-      while (tempDiv.hasChildNodes()) {
-        $head.append(tempDiv.firstChild);
+
+    // Add a new div representing the brush.
+    function addDiv() {
+      if ($div) $div.remove();
+
+      // Start hidden; we'll show it when movement occurs
+      $div = $(document.createElement('div')).attr('id', el.id + '_brush').css({
+        'background-color': opts.brushFill,
+        'opacity': opts.brushOpacity,
+        'pointer-events': 'none',
+        'position': 'absolute'
+      }).hide();
+
+      var borderStyle = '1px solid ' + opts.brushStroke;
+      if (opts.brushDirection === 'xy') {
+        $div.css({
+          'border': borderStyle
+        });
+      } else if (opts.brushDirection === 'x') {
+        $div.css({
+          'border-left': borderStyle,
+          'border-right': borderStyle
+        });
+      } else if (opts.brushDirection === 'y') {
+        $div.css({
+          'border-top': borderStyle,
+          'border-bottom': borderStyle
+        });
       }
+
+      $el.append($div);
+      $div.offset({ x: 0, y: 0 }).width(0).outerHeight(0);
     }
-  },
-  // Reads HTML and returns an object with info about singletons
-  _processHtml: function(val) {
-    var self = this;
-    var newSingletons = {};
-    var newVal;
-
-    var findNewPayload = function(match, p1, sig, payload) {
-      if (self.knownSingletons[sig] || newSingletons[sig])
-        return "";
-      newSingletons[sig] = true;
-      return payload;
-    };
-    while (true) {
-      newVal = val.replace(self._reSingleton, findNewPayload);
-      if (val.length === newVal.length)
-        break;
-      val = newVal;
+
+    // Update the brush div to reflect the current brush bounds.
+    function updateDiv() {
+      // Need parent offset relative to page to calculate mouse offset
+      // relative to page.
+      var imgOffset = $el.offset();
+      var b = state.boundsPx;
+      $div.offset({
+        top: imgOffset.top + b.ymin,
+        left: imgOffset.left + b.xmin
+      }).outerWidth(b.xmax - b.xmin + 1).outerHeight(b.ymax - b.ymin + 1);
     }
 
-    var heads = [];
-    var headAddPayload = function(match, payload) {
-      heads.push(payload);
-      return "";
-    };
-    while (true) {
-      newVal = val.replace(self._reHead, headAddPayload);
-      if (val.length === newVal.length)
-        break;
-      val = newVal;
+    function down(offset) {
+      if (offset === undefined) return state.down;
+
+      state.down = offset;
+      return undefined;
     }
 
-    return {
-      html: val,
-      head: heads.join("\n"),
-      singletons: newSingletons
-    };
-  },
-  _reSingleton: /<!--(SHINY.SINGLETON\[([\w]+)\])-->([\s\S]*?)<!--\/\1-->/,
-  _reHead: /<head(?:\s[^>]*)?>([\s\S]*?)<\/head>/
-};
+    function up(offset) {
+      if (offset === undefined) return state.up;
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/output_binding_downloadlink.js
-
-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');
+      state.up = offset;
+      return undefined;
+    }
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/output_binding_datatable.js
-
-var datatableOutputBinding = new OutputBinding();
-$.extend(datatableOutputBinding, {
-  find: function(scope) {
-    return $(scope).find('.shiny-datatable-output');
-  },
-  onValueError: function(el, err) {
-    exports.unbindAll(el);
-    this.renderError(el, err);
-  },
-  renderValue: function(el, data) {
-    var $el = $(el).empty();
-    if (!data || !data.colnames) return;
-
-    var colnames = $.makeArray(data.colnames);
-    var header = $.map(colnames, function(x) {
-      return '<th>' + x + '</th>';
-    }).join('');
-    header = '<thead><tr>' + header + '</tr></thead>';
-    var footer = '';
-    if (data.options === null || data.options.searching !== false) {
-      footer = $.map(colnames, function(x) {
-        // placeholder needs to be escaped (and HTML tags are stripped off)
-        return '<th><input type="text" placeholder="' +
-               escapeHTML(x.replace(/(<([^>]+)>)/ig, '')) +
-               '" /></th>';
-      }).join('');
-      footer = '<tfoot>' + footer + '</tfoot>';
+    function isBrushing() {
+      return state.brushing;
     }
-    var content = '<table class="table table-striped table-hover">' +
-                  header + footer + '</table>';
-    $el.append(content);
 
-    // options that should be eval()ed
-    if (data.evalOptions)
-      $.each(data.evalOptions, function(i, x) {
-        /*jshint evil: true */
-        data.options[x] = eval('(' + data.options[x] + ')');
-      });
+    function startBrushing() {
+      state.brushing = true;
+      addDiv();
+      state.panel = coordmap.getPanel(state.down, expandPixels);
 
-    // caseInsensitive searching? default true
-    var searchCI = data.options === null || typeof(data.options.search) === 'undefined' ||
-                   data.options.search.caseInsensitive !== false;
-    var oTable = $(el).children("table").DataTable($.extend({
-      "processing": true,
-      "serverSide": true,
-      "order": [],
-      "orderClasses": false,
-      "pageLength": 25,
-      "ajax": {
-        "url": data.action,
-        "type": "POST",
-        "data": function(d) {
-          d.search.caseInsensitive = searchCI;
-          d.escape = data.escape;
-        }
-      }
-    }, data.options));
-    // the table object may need post-processing
-    if (typeof data.callback === 'string') {
-      /*jshint evil: true */
-      var callback = eval('(' + data.callback + ')');
-      if (typeof callback === 'function') callback(oTable);
+      boundsPx(coordmap.findBox(state.down, state.down));
+      updateDiv();
     }
 
-    // use debouncing for searching boxes
-    $el.find('label input').first().unbind('keyup')
-         .keyup(debounce(data.searchDelay, function() {
-            oTable.search(this.value).draw();
-          }));
-    var searchInputs = $el.find("tfoot input");
-    if (searchInputs.length > 0) {
-      // this is a little weird: aoColumns/bSearchable are still in DT 1.10
-      // https://github.com/DataTables/DataTables/issues/388
-      $.each(oTable.settings()[0].aoColumns, function(i, x) {
-        // hide the text box if not searchable
-        if (!x.bSearchable) searchInputs.eq(i).hide();
-      });
-      searchInputs.keyup(debounce(data.searchDelay, function() {
-        oTable.column(searchInputs.index(this)).search(this.value).draw();
-      }));
+    function brushTo(offset) {
+      boundsPx(coordmap.findBox(state.down, offset));
+      $div.show();
+      updateDiv();
     }
-    // FIXME: ugly scrollbars in tab panels b/c Bootstrap uses 'visible: auto'
-    $el.parents('.tab-content').css('overflow', 'visible');
-  }
-});
-outputBindings.register(datatableOutputBinding, 'shiny.datatableOutput');
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/output_binding_adapter.js
+    function stopBrushing() {
+      state.brushing = false;
 
-var OutputBindingAdapter = function(el, binding) {
-  this.el = el;
-  this.binding = binding;
+      // Save the final bounding box of the brush
+      boundsPx(coordmap.findBox(state.down, state.up));
+    }
 
-  // If the binding actually has a resize method, override the prototype of
-  // onResize with a version that does a makeResizeFilter on the element.
-  if (binding.resize) {
-    this.onResize = makeResizeFilter(el, function(width, height) {
-      binding.resize(el, width, height);
-    });
-  }
-};
-(function() {
-  this.getId = function() {
-    return this.binding.getId(this.el);
-  };
-  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);
-  };
-  this.onResize = function() {
-    // Intentionally left blank; see constructor
-  };
-}).call(OutputBindingAdapter.prototype);
+    function isDragging() {
+      return state.dragging;
+    }
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding.js
+    function startDragging() {
+      state.dragging = true;
+      state.changeStartBounds = $.extend({}, state.boundsPx);
+    }
+
+    function dragTo(offset) {
+      // How far the brush was dragged
+      var dx = offset.x - state.down.x;
+      var dy = offset.y - state.down.y;
 
-var InputBinding = exports.InputBinding = function() {};
+      // Calculate what new positions would be, before clipping.
+      var start = state.changeStartBounds;
+      var newBounds = {
+        xmin: start.xmin + dx,
+        xmax: start.xmax + dx,
+        ymin: start.ymin + dy,
+        ymax: start.ymax + dy
+      };
 
-(function() {
+      // Clip to the plotting area
+      if (opts.brushClip) {
+        var panelBounds = state.panel.range;
 
-// Returns a jQuery object or element array that contains the
-// descendants of scope that match this binding
-this.find = function(scope) { throw "Not implemented"; };
+        // Convert to format for shiftToRange
+        var xvals = [newBounds.xmin, newBounds.xmax];
+        var yvals = [newBounds.ymin, newBounds.ymax];
 
-this.getId = function(el) {
-  return el['data-input-id'] || el.id;
-};
+        xvals = coordmap.shiftToRange(xvals, panelBounds.left, panelBounds.right);
+        yvals = coordmap.shiftToRange(yvals, panelBounds.top, panelBounds.bottom);
 
-// 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, callback) { };
-this.unsubscribe = function(el) { };
+        // Convert back to bounds format
+        newBounds = {
+          xmin: xvals[0],
+          xmax: xvals[1],
+          ymin: yvals[0],
+          ymax: yvals[1]
+        };
+      }
 
-// This is used for receiving messages that tell the input object to do
-// things, such as setting values (including min, max, and others).
-// 'data' should be an object with elements corresponding to value, min,
-// max, etc., as appropriate for the type of input object. It also should
-// trigger a change event.
-this.receiveMessage = function(el, data) { throw "Not implemented"; };
-this.getState = function(el, data) { throw "Not implemented"; };
+      boundsPx(newBounds);
+      updateDiv();
+    }
 
-this.getRatePolicy = function() { return null; };
+    function stopDragging() {
+      state.dragging = false;
+    }
 
-// Some input objects need initialization before being bound. This is
-// called when the document is ready (for statically-added input objects),
-// and when new input objects are added to the document with
-// htmlOutputBinding.renderValue() (for dynamically-added input objects).
-// This is called before the input is bound.
-this.initialize = function(el) { };
+    function isResizing() {
+      return state.resizing;
+    }
 
-// This is called after unbinding the output.
-this.dispose = function(el) { };
+    function startResizing() {
+      state.resizing = true;
+      state.changeStartBounds = $.extend({}, state.boundsPx);
+      state.resizeSides = whichResizeSides(state.down);
+    }
 
-}).call(InputBinding.prototype);
+    function resizeTo(offset) {
+      // How far the brush was dragged
+      var dx = offset.x - state.down.x;
+      var dy = offset.y - state.down.y;
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_text.js
-
-var textInputBinding = new InputBinding();
-$.extend(textInputBinding, {
-  find: function(scope) {
-    return $(scope).find('input[type="text"], input[type="password"], input[type="search"], input[type="url"], input[type="email"]');
-  },
-  getId: function(el) {
-    return InputBinding.prototype.getId.call(this, el) || el.name;
-  },
-  getValue: function(el) {
-    return el.value;
-  },
-  setValue: function(el, value) {
-    el.value = value;
-  },
-  subscribe: function(el, callback) {
-    $(el).on('keyup.textInputBinding input.textInputBinding', function(event) {
-      callback(true);
-    });
-    $(el).on('change.textInputBinding', function(event) {
-      callback(false);
-    });
-  },
-  unsubscribe: function(el) {
-    $(el).off('.textInputBinding');
-  },
-  receiveMessage: function(el, data) {
-    if (data.hasOwnProperty('value'))
-      this.setValue(el, data.value);
-
-    if (data.hasOwnProperty('label'))
-      $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
-
-    $(el).trigger('change');
-  },
-  getState: function(el) {
-    return {
-      label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
-      value: el.value
-    };
-  },
-  getRatePolicy: function() {
-    return {
-      policy: 'debounce',
-      delay: 250
-    };
-  }
-});
-inputBindings.register(textInputBinding, 'shiny.textInput');
+      // Calculate what new positions would be, before clipping.
+      var b = $.extend({}, state.changeStartBounds);
+      var panelBounds = state.panel.range;
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_textarea.js
+      if (state.resizeSides.left) {
+        b.xmin = coordmap.shiftToRange([b.xmin + dx], panelBounds.left, b.xmax)[0];
+      } else if (state.resizeSides.right) {
+        b.xmax = coordmap.shiftToRange([b.xmax + dx], b.xmin, panelBounds.right)[0];
+      }
 
-var textareaInputBinding = {};
-$.extend(textareaInputBinding, textInputBinding, {
-  find: function(scope) {
-    return $(scope).find('textarea');
-  }
-});
-inputBindings.register(textareaInputBinding, 'shiny.textareaInput');
+      if (state.resizeSides.top) {
+        b.ymin = coordmap.shiftToRange([b.ymin + dy], panelBounds.top, b.ymax)[0];
+      } else if (state.resizeSides.bottom) {
+        b.ymax = coordmap.shiftToRange([b.ymax + dy], b.ymin, panelBounds.bottom)[0];
+      }
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_number.js
-
-var numberInputBinding = {};
-$.extend(numberInputBinding, textInputBinding, {
-  find: function(scope) {
-    return $(scope).find('input[type="number"]');
-  },
-  getValue: function(el) {
-    var numberVal = $(el).val();
-    if (/^\s*$/.test(numberVal))  // Return null if all whitespace
-      return null;
-    else if (!isNaN(numberVal))   // If valid Javascript number string, coerce to number
-      return +numberVal;
-    else
-      return numberVal;           // If other string like "1e6", send it unchanged
-  },
-  setValue: function(el, value) {
-    el.value = value;
-  },
-  getType: function(el) {
-    return "shiny.number";
-  },
-  receiveMessage: function(el, data) {
-    if (data.hasOwnProperty('value'))  el.value = data.value;
-    if (data.hasOwnProperty('min'))    el.min   = data.min;
-    if (data.hasOwnProperty('max'))    el.max   = data.max;
-    if (data.hasOwnProperty('step'))   el.step  = data.step;
-
-    if (data.hasOwnProperty('label'))
-      $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
-
-    $(el).trigger('change');
-  },
-  getState: function(el) {
-    return { label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
-             value: this.getValue(el),
-             min:   Number(el.min),
-             max:   Number(el.max),
-             step:  Number(el.step) };
-  }
-});
-inputBindings.register(numberInputBinding, 'shiny.numberInput');
+      boundsPx(b);
+      updateDiv();
+    }
+
+    function stopResizing() {
+      state.resizing = false;
+    }
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_checkbox.js
-
-var checkboxInputBinding = new InputBinding();
-$.extend(checkboxInputBinding, {
-  find: function(scope) {
-    return $(scope).find('input[type="checkbox"]');
-  },
-  getValue: function(el) {
-    return el.checked;
-  },
-  setValue: function(el, value) {
-    el.checked = value;
-  },
-  subscribe: function(el, callback) {
-    $(el).on('change.checkboxInputBinding', function(event) {
-      callback(true);
-    });
-  },
-  unsubscribe: function(el) {
-    $(el).off('.checkboxInputBinding');
-  },
-  getState: function(el) {
     return {
-      label: $(el).parent().find('span').text(),
-      value: el.checked
+      reset: reset,
+
+      importOldBrush: importOldBrush,
+      isInsideBrush: isInsideBrush,
+      isInResizeArea: isInResizeArea,
+      whichResizeSides: whichResizeSides,
+
+      boundsPx: boundsPx,
+      boundsData: boundsData,
+      getPanel: getPanel,
+
+      down: down,
+      up: up,
+
+      isBrushing: isBrushing,
+      startBrushing: startBrushing,
+      brushTo: brushTo,
+      stopBrushing: stopBrushing,
+
+      isDragging: isDragging,
+      startDragging: startDragging,
+      dragTo: dragTo,
+      stopDragging: stopDragging,
+
+      isResizing: isResizing,
+      startResizing: startResizing,
+      resizeTo: resizeTo,
+      stopResizing: stopResizing
     };
-  },
-  receiveMessage: function(el, data) {
-    if (data.hasOwnProperty('value'))
-      el.checked = data.value;
+  };
 
-    if (data.hasOwnProperty('label'))
-      $(el).parent().find('span').text(data.label);
+  exports.resetBrush = function (brushId) {
+    exports.onInputChange(brushId, null);
+    imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
+      brushId: brushId, outputId: null
+    });
+  };
 
-    $(el).trigger('change');
-  }
-});
-inputBindings.register(checkboxInputBinding, 'shiny.checkboxInput');
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/output_binding_html.js
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_slider.js
-
-// Necessary to get hidden sliders to send their updated values
-function forceIonSliderUpdate(slider) {
-  if (slider.$cache && slider.$cache.input)
-    slider.$cache.input.trigger('change');
-  else
-    console.log("Couldn't force ion slider to update");
-}
-
-var sliderInputBinding = {};
-$.extend(sliderInputBinding, textInputBinding, {
-  find: function(scope) {
-    // Check if ionRangeSlider plugin is loaded
-    if (!$.fn.ionRangeSlider)
-      return [];
-
-    return $(scope).find('input.js-range-slider');
-  },
-  getType: function(el) {
-    var dataType = $(el).data('data-type');
-    if (dataType === 'date')
-      return 'shiny.date';
-    else if (dataType === 'datetime')
-      return 'shiny.datetime';
-    else
-      return false;
-  },
-  getValue: function(el) {
-    var $el = $(el);
-    var result = $(el).data('ionRangeSlider').result;
-
-    // Function for converting numeric value from slider to appropriate type.
-    var convert;
-    var dataType = $el.data('data-type');
-    if (dataType === 'date') {
-      convert = function(val) {
-        return formatDateUTC(new Date(+val));
-      };
-    } else if (dataType === 'datetime') {
-      convert = function(val) {
-        // Convert ms to s
-        return +val / 1000;
-      };
-    } else {
-      convert = function(val) { return +val; };
+  var htmlOutputBinding = new OutputBinding();
+  $.extend(htmlOutputBinding, {
+    find: function find(scope) {
+      return $(scope).find('.shiny-html-output');
+    },
+    onValueError: function onValueError(el, err) {
+      exports.unbindAll(el);
+      this.renderError(el, err);
+    },
+    renderValue: function renderValue(el, data) {
+      exports.renderContent(el, data);
     }
+  });
+  outputBindings.register(htmlOutputBinding, 'shiny.htmlOutput');
 
-    if (this._numValues(el) == 2) {
-      return [convert(result.from), convert(result.to)];
+  var renderDependencies = exports.renderDependencies = function (dependencies) {
+    if (dependencies) {
+      $.each(dependencies, function (i, dep) {
+        renderDependency(dep);
+      });
     }
-    else {
-      return convert(result.from);
+  };
+
+  // Render HTML in a DOM element, add dependencies, and bind Shiny
+  // inputs/outputs. `content` can be null, a string, or an object with
+  // properties 'html' and 'deps'.
+  exports.renderContent = function (el, content) {
+    var where = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "replace";
+
+    exports.unbindAll(el);
+
+    var html;
+    var dependencies = [];
+    if (content === null) {
+      html = '';
+    } else if (typeof content === 'string') {
+      html = content;
+    } else if ((typeof content === 'undefined' ? 'undefined' : _typeof(content)) === 'object') {
+      html = content.html;
+      dependencies = content.deps || [];
     }
 
-  },
-  setValue: function(el, value) {
-    var slider = $(el).data('ionRangeSlider');
+    exports.renderHtml(html, el, dependencies, where);
 
-    if (this._numValues(el) == 2 && value instanceof Array) {
-      slider.update({ from: value[0], to: value[1] });
+    var scope = el;
+    if (where === "replace") {
+      exports.initializeInputs(el);
+      exports.bindAll(el);
     } else {
-      slider.update({ from: value });
-    }
-    forceIonSliderUpdate(slider);
-  },
-  subscribe: function(el, callback) {
-    $(el).on('change.sliderInputBinding', function(event) {
-      callback(!$(el).data('updating') && !$(el).data('animating'));
-    });
-  },
-  unsubscribe: function(el) {
-    $(el).off('.sliderInputBinding');
-  },
-  receiveMessage: function(el, data) {
-    var $el = $(el);
-    var slider = $el.data('ionRangeSlider');
-    var msg = {};
-
-    if (data.hasOwnProperty('value')) {
-      if (this._numValues(el) == 2 && data.value instanceof Array) {
-        msg.from = data.value[0];
-        msg.to = data.value[1];
-      } else {
-        msg.from = data.value;
+      var $parent = $(el).parent();
+      if ($parent.length > 0) {
+        scope = $parent;
+        if (where === "beforeBegin" || where === "afterEnd") {
+          var $grandparent = $parent.parent();
+          if ($grandparent.length > 0) scope = $grandparent;
+        }
       }
+      exports.initializeInputs(scope);
+      exports.bindAll(scope);
     }
-    if (data.hasOwnProperty('min'))  msg.min   = data.min;
-    if (data.hasOwnProperty('max'))  msg.max   = data.max;
-    if (data.hasOwnProperty('step')) msg.step  = data.step;
+  };
 
-    if (data.hasOwnProperty('label'))
-      $el.parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
+  // Render HTML in a DOM element, inserting singletons into head as needed
+  exports.renderHtml = function (html, el, dependencies) {
+    var where = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'replace';
 
-    $el.data('updating', true);
-    try {
-      slider.update(msg);
-      forceIonSliderUpdate(slider);
-    } finally {
-      $el.data('updating', false);
-    }
-  },
-  getRatePolicy: function() {
-    return {
-      policy: 'debounce',
-      delay: 250
-    };
-  },
-  getState: function(el) {
-  },
-  initialize: function(el) {
-    var opts = {};
-    var $el = $(el);
-    var dataType = $el.data('data-type');
-    var timeFormat = $el.data('time-format');
-    var timeFormatter;
-
-    // Set up formatting functions
-    if (dataType === 'date') {
-      timeFormatter = strftime.utc();
-      opts.prettify = function(num) {
-        return timeFormatter(timeFormat, new Date(num));
-      };
+    renderDependencies(dependencies);
+    return singletons.renderHtml(html, el, where);
+  };
 
-    } else if (dataType === 'datetime') {
-      var timezone = $el.data('timezone');
-      if (timezone)
-        timeFormatter = strftime.timezone(timezone);
-      else
-        timeFormatter = strftime;
+  var htmlDependencies = {};
+  function registerDependency(name, version) {
+    htmlDependencies[name] = version;
+  }
 
-      opts.prettify = function(num) {
-        return timeFormatter(timeFormat, new Date(num));
-      };
+  // Client-side dependency resolution and rendering
+  function renderDependency(dep) {
+    if (htmlDependencies.hasOwnProperty(dep.name)) return false;
+
+    registerDependency(dep.name, dep.version);
+
+    var href = dep.src.href;
+
+    var $head = $("head").first();
+
+    if (dep.meta) {
+      var metas = $.map(asArray(dep.meta), function (content, name) {
+        return $("<meta>").attr("name", name).attr("content", content);
+      });
+      $head.append(metas);
     }
 
-    $el.ionRangeSlider(opts);
-  },
+    if (dep.stylesheet) {
+      var stylesheets = $.map(asArray(dep.stylesheet), function (stylesheet) {
+        return $("<link rel='stylesheet' type='text/css'>").attr("href", href + "/" + encodeURI(stylesheet));
+      });
+      $head.append(stylesheets);
+    }
 
-  // Number of values; 1 for single slider, 2 for range slider
-  _numValues: function(el) {
-    if ($(el).data('ionRangeSlider').options.type === 'double')
-      return 2;
-    else
-      return 1;
+    if (dep.script) {
+      var scripts = $.map(asArray(dep.script), function (scriptName) {
+        return $("<script>").attr("src", href + "/" + encodeURI(scriptName));
+      });
+      $head.append(scripts);
+    }
+
+    if (dep.attachment) {
+      // dep.attachment might be a single string, an array, or an object.
+      var attachments = dep.attachment;
+      if (typeof attachments === "string") attachments = [attachments];
+      if ($.isArray(attachments)) {
+        // The contract for attachments is that arrays of attachments are
+        // addressed using 1-based indexes. Convert this array to an object.
+        var tmp = {};
+        $.each(attachments, function (index, attachment) {
+          tmp[index + 1 + ""] = attachment;
+        });
+        attachments = tmp;
+      }
+
+      var attach = $.map(attachments, function (attachment, key) {
+        return $("<link rel='attachment'>").attr("id", dep.name + "-" + key + "-attachment").attr("href", href + "/" + encodeURI(attachment));
+      });
+      $head.append(attach);
+    }
+
+    if (dep.head) {
+      var $newHead = $("<head></head>");
+      $newHead.html(dep.head);
+      $head.append($newHead.children());
+    }
+    return true;
   }
-});
-inputBindings.register(sliderInputBinding, 'shiny.sliderInput');
-
-
-
-$(document).on('click', '.slider-animate-button', function(evt) {
-  evt.preventDefault();
-  var self = $(this);
-  var target = $('#' + $escape(self.attr('data-target-id')));
-  var startLabel = 'Play';
-  var stopLabel = 'Pause';
-  var loop = self.attr('data-loop') !== undefined &&
-             !/^\s*false\s*$/i.test(self.attr('data-loop'));
-  var animInterval = self.attr('data-interval');
-  if (isNaN(animInterval))
-    animInterval = 1500;
-  else
-    animInterval = +animInterval;
-
-  if (!target.data('animTimer')) {
-    var slider;
-    var timer;
-
-    // Separate code paths:
-    // Backward compatible code for old-style jsliders (Shiny <= 0.10.2.2),
-    // and new-style ionsliders.
-    if (target.hasClass('jslider')) {
-      slider = target.slider();
-
-      // If we're currently at the end, restart
-      if (!slider.canStepNext())
-        slider.resetToStart();
-
-      timer = setInterval(function() {
-        if (loop && !slider.canStepNext()) {
-          slider.resetToStart();
+
+  var singletons = {
+    knownSingletons: {},
+    renderHtml: function renderHtml(html, el, where) {
+      var processed = this._processHtml(html);
+      this._addToHead(processed.head);
+      this.register(processed.singletons);
+      if (where === "replace") {
+        $(el).html(processed.html);
+      } else {
+        el.insertAdjacentHTML(where, processed.html);
+      }
+      return processed;
+    },
+    // Take an object where keys are names of singletons, and merges it into
+    // knownSingletons
+    register: function register(s) {
+      $.extend(this.knownSingletons, s);
+    },
+    // Takes a string or array of strings and adds them to knownSingletons
+    registerNames: function registerNames(s) {
+      if (typeof s === 'string') {
+        this.knownSingletons[s] = true;
+      } else if (s instanceof Array) {
+        for (var i = 0; i < s.length; i++) {
+          this.knownSingletons[s[i]] = true;
         }
-        else {
-          slider.stepNext();
-          if (!loop && !slider.canStepNext()) {
-            self.click(); // stop the animation
-          }
+      }
+    },
+    // Inserts new content into document head
+    _addToHead: function _addToHead(head) {
+      if (head.length > 0) {
+        var tempDiv = $("<div>" + head + "</div>")[0];
+        var $head = $('head');
+        while (tempDiv.hasChildNodes()) {
+          $head.append(tempDiv.firstChild);
         }
-      }, animInterval);
-
-    } else {
-      slider = target.data('ionRangeSlider');
-      // Single sliders have slider.options.type == "single", and only the
-      // `from` value is used. Double sliders have type == "double", and also
-      // use the `to` value for the right handle.
-      var sliderCanStep = function() {
-        if (slider.options.type === "double")
-          return slider.result.to < slider.result.max;
-        else
-          return slider.result.from < slider.result.max;
+      }
+    },
+    // Reads HTML and returns an object with info about singletons
+    _processHtml: function _processHtml(val) {
+      var self = this;
+      var newSingletons = {};
+      var newVal;
+
+      var findNewPayload = function findNewPayload(match, p1, sig, payload) {
+        if (self.knownSingletons[sig] || newSingletons[sig]) return "";
+        newSingletons[sig] = true;
+        return payload;
       };
-      var sliderReset = function() {
-        var val = { from: slider.result.min };
-        // Preserve the current spacing for double sliders
-        if (slider.options.type === "double")
-          val.to = val.from + (slider.result.to - slider.result.from);
+      while (true) {
+        newVal = val.replace(self._reSingleton, findNewPayload);
+        if (val.length === newVal.length) break;
+        val = newVal;
+      }
 
-        slider.update(val);
-        forceIonSliderUpdate(slider);
+      var heads = [];
+      var headAddPayload = function headAddPayload(match, payload) {
+        heads.push(payload);
+        return "";
       };
-      var sliderStep = function() {
-        // Don't overshoot the end
-        var val = {
-          from: Math.min(slider.result.max, slider.result.from + slider.options.step)
-        };
-        if (slider.options.type === "double")
-          val.to = Math.min(slider.result.max, slider.result.to + slider.options.step);
+      while (true) {
+        newVal = val.replace(self._reHead, headAddPayload);
+        if (val.length === newVal.length) break;
+        val = newVal;
+      }
 
-        slider.update(val);
-        forceIonSliderUpdate(slider);
+      return {
+        html: val,
+        head: heads.join("\n"),
+        singletons: newSingletons
       };
+    },
+    _reSingleton: /<!--(SHINY.SINGLETON\[([\w]+)\])-->([\s\S]*?)<!--\/\1-->/,
+    _reHead: /<head(?:\s[^>]*)?>([\s\S]*?)<\/head>/
+  };
 
-      // If we're currently at the end, restart
-      if (!sliderCanStep())
-        sliderReset();
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/output_binding_downloadlink.js
 
-      timer = setInterval(function() {
-        if (loop && !sliderCanStep()) {
-          sliderReset();
-        }
-        else {
-          sliderStep();
-          if (!loop && !sliderCanStep()) {
-            self.click(); // stop the animation
-          }
-        }
-      }, animInterval);
+  var downloadLinkOutputBinding = new OutputBinding();
+  $.extend(downloadLinkOutputBinding, {
+    find: function find(scope) {
+      return $(scope).find('a.shiny-download-link');
+    },
+    renderValue: function renderValue(el, data) {
+      $(el).attr('href', data);
     }
+  });
+  outputBindings.register(downloadLinkOutputBinding, 'shiny.downloadLink');
 
-    target.data('animTimer', timer);
-    self.attr('title', stopLabel);
-    self.addClass('playing');
-    target.data('animating', true);
-  }
-  else {
-    clearTimeout(target.data('animTimer'));
-    target.removeData('animTimer');
-    self.attr('title', startLabel);
-    self.removeClass('playing');
-    target.removeData('animating');
-  }
-});
-
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_date.js
-
-var dateInputBinding = new InputBinding();
-$.extend(dateInputBinding, {
-  find: function(scope) {
-    return $(scope).find('.shiny-date-input');
-  },
-  getType: function(el) {
-    return "shiny.date";
-  },
-  // Return the date in an unambiguous format, yyyy-mm-dd (as opposed to a
-  // format like mm/dd/yyyy)
-  getValue: function(el) {
-    var date = $(el).find('input').data('datepicker').getUTCDate();
-    return formatDateUTC(date);
-  },
-  // value must be an unambiguous string like '2001-01-01', or a Date object.
-  setValue: function(el, value) {
-    var date = this._newDate(value);
-    // If date is invalid, do nothing
-    if (isNaN(date))
-      return;
-
-    $(el).find('input').datepicker('update', date);
-  },
-  getState: function(el) {
-    var $el = $(el);
-    var $input = $el.find('input');
-
-    var min = $input.data('datepicker').startDate;
-    var max = $input.data('datepicker').endDate;
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/output_binding_datatable.js
 
-    // Stringify min and max. If min and max aren't set, they will be
-    // -Infinity and Infinity; replace these with null.
-    min = (min === -Infinity) ? null : formatDateUTC(min);
-    max = (max ===  Infinity) ? null : formatDateUTC(max);
+  var datatableOutputBinding = new OutputBinding();
+  $.extend(datatableOutputBinding, {
+    find: function find(scope) {
+      return $(scope).find('.shiny-datatable-output');
+    },
+    onValueError: function onValueError(el, err) {
+      exports.unbindAll(el);
+      this.renderError(el, err);
+    },
+    renderValue: function renderValue(el, _data) {
+      var $el = $(el).empty();
+      if (!_data || !_data.colnames) return;
 
-    // startViewMode is stored as a number; convert to string
-    var startview = $input.data('datepicker').startViewMode;
-    if      (startview === 2)  startview = 'decade';
-    else if (startview === 1)  startview = 'year';
-    else if (startview === 0)  startview = 'month';
+      var colnames = $.makeArray(_data.colnames);
+      var header = $.map(colnames, function (x) {
+        return '<th>' + x + '</th>';
+      }).join('');
+      header = '<thead><tr>' + header + '</tr></thead>';
+      var footer = '';
+      if (_data.options === null || _data.options.searching !== false) {
+        footer = $.map(colnames, function (x) {
+          // placeholder needs to be escaped (and HTML tags are stripped off)
+          return '<th><input type="text" placeholder="' + escapeHTML(x.replace(/(<([^>]+)>)/ig, '')) + '" /></th>';
+        }).join('');
+        footer = '<tfoot>' + footer + '</tfoot>';
+      }
+      var content = '<table class="table table-striped table-hover">' + header + footer + '</table>';
+      $el.append(content);
 
-    return {
-      label:       $el.find('label[for="' + $escape(el.id) + '"]').text(),
-      value:       this.getValue(el),
-      valueString: $input.val(),
-      min:         min,
-      max:         max,
-      language:    $input.data('datepicker').language,
-      weekstart:   $input.data('datepicker').weekStart,
-      format:      this._formatToString($input.data('datepicker').format),
-      startview:   startview
-    };
-  },
-  receiveMessage: function(el, data) {
-    var $input = $(el).find('input');
+      // options that should be eval()ed
+      if (_data.evalOptions) $.each(_data.evalOptions, function (i, x) {
+        /*jshint evil: true */
+        _data.options[x] = eval('(' + _data.options[x] + ')');
+      });
 
-    if (data.hasOwnProperty('value'))
-      this.setValue(el, data.value);
+      // caseInsensitive searching? default true
+      var searchCI = _data.options === null || typeof _data.options.search === 'undefined' || _data.options.search.caseInsensitive !== false;
+      var oTable = $(el).children("table").DataTable($.extend({
+        "processing": true,
+        "serverSide": true,
+        "order": [],
+        "orderClasses": false,
+        "pageLength": 25,
+        "ajax": {
+          "url": _data.action,
+          "type": "POST",
+          "data": function data(d) {
+            d.search.caseInsensitive = searchCI;
+            d.escape = _data.escape;
+          }
+        }
+      }, _data.options));
+      // the table object may need post-processing
+      if (typeof _data.callback === 'string') {
+        /*jshint evil: true */
+        var callback = eval('(' + _data.callback + ')');
+        if (typeof callback === 'function') callback(oTable);
+      }
 
-    if (data.hasOwnProperty('label'))
-      $(el).find('label[for="' + $escape(el.id) + '"]').text(data.label);
+      // use debouncing for searching boxes
+      $el.find('label input').first().unbind('keyup').keyup(debounce(_data.searchDelay, function () {
+        oTable.search(this.value).draw();
+      }));
+      var searchInputs = $el.find("tfoot input");
+      if (searchInputs.length > 0) {
+        // this is a little weird: aoColumns/bSearchable are still in DT 1.10
+        // https://github.com/DataTables/DataTables/issues/388
+        $.each(oTable.settings()[0].aoColumns, function (i, x) {
+          // hide the text box if not searchable
+          if (!x.bSearchable) searchInputs.eq(i).hide();
+        });
+        searchInputs.keyup(debounce(_data.searchDelay, function () {
+          oTable.column(searchInputs.index(this)).search(this.value).draw();
+        }));
+      }
+      // FIXME: ugly scrollbars in tab panels b/c Bootstrap uses 'visible: auto'
+      $el.parents('.tab-content').css('overflow', 'visible');
+    }
+  });
+  outputBindings.register(datatableOutputBinding, 'shiny.datatableOutput');
 
-    if (data.hasOwnProperty('min'))
-      this._setMin($input[0], data.min);
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/output_binding_adapter.js
 
-    if (data.hasOwnProperty('max'))
-      this._setMax($input[0], data.max);
+  var OutputBindingAdapter = function OutputBindingAdapter(el, binding) {
+    this.el = el;
+    this.binding = binding;
 
-    $(el).trigger('change');
-  },
-  subscribe: function(el, callback) {
-    $(el).on('keyup.dateInputBinding input.dateInputBinding', function(event) {
-      // Use normal debouncing policy when typing
-      callback(true);
-    });
-    $(el).on('changeDate.dateInputBinding change.dateInputBinding', function(event) {
-      // Send immediately when clicked
-      callback(false);
-    });
-  },
-  unsubscribe: function(el) {
-    $(el).off('.dateInputBinding');
-  },
-  getRatePolicy: function() {
-    return {
-      policy: 'debounce',
-      delay: 250
-    };
-  },
-  initialize: function(el) {
-    var $input = $(el).find('input');
-
-    var date = $input.data('initial-date');
-    // If initial_date is null, set to current date
-    if (date === undefined || date === null) {
-      // Get local date, but as UTC
-      date = this._dateAsUTC(new Date());
+    // If the binding actually has a resize method, override the prototype of
+    // onResize with a version that does a makeResizeFilter on the element.
+    if (binding.resize) {
+      this.onResize = makeResizeFilter(el, function (width, height) {
+        binding.resize(el, width, height);
+      });
     }
+  };
+  (function () {
+    this.getId = function () {
+      return this.binding.getId(this.el);
+    };
+    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);
+    };
+    this.onResize = function () {
+      // Intentionally left blank; see constructor
+    };
+  }).call(OutputBindingAdapter.prototype);
 
-    this.setValue(el, date);
-
-    // Set the start and end dates, from min-date and max-date. These always
-    // use yyyy-mm-dd format, instead of bootstrap-datepicker's built-in
-    // support for date-startdate and data-enddate, which use the current
-    // date format.
-    this._setMin($input[0], $input.data('min-date'));
-    this._setMax($input[0], $input.data('max-date'));
-  },
-  // Given a format object from a date picker, return a string
-  _formatToString: function(format) {
-    // Format object has structure like:
-    // { parts: ['mm', 'dd', 'yy'], separators: ['', '/', '/' ,''] }
-    var str = '';
-    for (var i = 0; i < format.parts.length; i++) {
-      str += format.separators[i] + format.parts[i];
-    }
-    str += format.separators[i];
-    return str;
-  },
-  // Given an unambiguous date string or a Date object, set the min (start) date.
-  // null will unset.
-  _setMin: function(el, date) {
-    if (date === null) {
-      $(el).datepicker('setStartDate', null);
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding.js
 
-    } else {
-      date = this._newDate(date);
-      if (!isNaN(date))
-        $(el).datepicker('setStartDate', date);
-    }
-  },
-  // Given an unambiguous date string or a Date object, set the max (end) date
-  // null will unset.
-  _setMax: function(el, date) {
-    if (date === null) {
-      $(el).datepicker('setEndDate', null);
+  var InputBinding = exports.InputBinding = function () {};
 
-    } else {
-      date = this._newDate(date);
-      if (!isNaN(date))
-        $(el).datepicker('setEndDate', date);
-    }
-  },
-  // Given a date string of format yyyy-mm-dd, return a Date object with
-  // that date at 12AM UTC.
-  // If date is a Date object, return it unchanged.
-  _newDate: function(date) {
-    if (date instanceof Date)
-      return date;
-    if (!date)
-      return null;
+  (function () {
+
+    // Returns a jQuery object or element array that contains the
+    // descendants of scope that match this binding
+    this.find = function (scope) {
+      throw "Not implemented";
+    };
 
-    // Get Date object - this will be at 12AM in UTC, but may print
-    // differently at the Javascript console.
-    var d = parseDate(date);
+    this.getId = function (el) {
+      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";
+    };
 
-    // If invalid date, return null
-    if (isNaN(d))
+    // The callback method takes one argument, whose value is boolean. If true,
+    // allow deferred (debounce or throttle) sending depending on the value of
+    // getRatePolicy. If false, send value immediately.
+    this.subscribe = function (el, callback) {};
+    this.unsubscribe = function (el) {};
+
+    // This is used for receiving messages that tell the input object to do
+    // things, such as setting values (including min, max, and others).
+    // 'data' should be an object with elements corresponding to value, min,
+    // max, etc., as appropriate for the type of input object. It also should
+    // trigger a change event.
+    this.receiveMessage = function (el, data) {
+      throw "Not implemented";
+    };
+    this.getState = function (el, data) {
+      throw "Not implemented";
+    };
+
+    this.getRatePolicy = function () {
       return null;
+    };
 
-    return new Date(d.getTime());
-  },
-  // Given a Date object, return a Date object which has the same "clock time"
-  // in UTC. For example, if input date is 2013-02-01 23:00:00 GMT-0600 (CST),
-  // output will be 2013-02-01 23:00:00 UTC. Note that the JS console may
-  // print this in local time, as "Sat Feb 02 2013 05:00:00 GMT-0600 (CST)".
-  _dateAsUTC: function(date) {
-    return new Date(date.getTime() - date.getTimezoneOffset() * 60000);
-  }
-});
-inputBindings.register(dateInputBinding, 'shiny.dateInput');
+    // Some input objects need initialization before being bound. This is
+    // called when the document is ready (for statically-added input objects),
+    // and when new input objects are added to the document with
+    // htmlOutputBinding.renderValue() (for dynamically-added input objects).
+    // This is called before the input is bound.
+    this.initialize = function (el) {};
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_daterange.js
-
-var dateRangeInputBinding = {};
-$.extend(dateRangeInputBinding, dateInputBinding, {
-  find: function(scope) {
-    return $(scope).find('.shiny-date-range-input');
-  },
-  // Return the date in an unambiguous format, yyyy-mm-dd (as opposed to a
-  // format like mm/dd/yyyy)
-  getValue: function(el) {
-    var $inputs = $(el).find('input');
-    var start = $inputs.eq(0).data('datepicker').getUTCDate();
-    var end   = $inputs.eq(1).data('datepicker').getUTCDate();
-
-    return [formatDateUTC(start), formatDateUTC(end)];
-  },
-  // value must be an array of unambiguous strings like '2001-01-01', or
-  // Date objects.
-  setValue: function(el, value) {
-    if (!(value instanceof Array)) {
-      return;
+    // This is called after unbinding the output.
+    this.dispose = function (el) {};
+  }).call(InputBinding.prototype);
+
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_text.js
+
+  var textInputBinding = new InputBinding();
+  $.extend(textInputBinding, {
+    find: function find(scope) {
+      return $(scope).find('input[type="text"], input[type="search"], input[type="url"], input[type="email"]');
+    },
+    getId: function getId(el) {
+      return InputBinding.prototype.getId.call(this, el) || el.name;
+    },
+    getValue: function getValue(el) {
+      return el.value;
+    },
+    setValue: function setValue(el, value) {
+      el.value = value;
+    },
+    subscribe: function subscribe(el, callback) {
+      $(el).on('keyup.textInputBinding input.textInputBinding', function (event) {
+        callback(true);
+      });
+      $(el).on('change.textInputBinding', function (event) {
+        callback(false);
+      });
+    },
+    unsubscribe: function unsubscribe(el) {
+      $(el).off('.textInputBinding');
+    },
+    receiveMessage: function receiveMessage(el, data) {
+      if (data.hasOwnProperty('value')) this.setValue(el, data.value);
+
+      if (data.hasOwnProperty('label')) $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
+
+      $(el).trigger('change');
+    },
+    getState: function getState(el) {
+      return {
+        label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
+        value: el.value
+      };
+    },
+    getRatePolicy: function getRatePolicy() {
+      return {
+        policy: 'debounce',
+        delay: 250
+      };
     }
+  });
+  inputBindings.register(textInputBinding, 'shiny.textInput');
 
-    // Get the start and end input objects
-    var $inputs = $(el).find('input');
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_textarea.js
 
-    // If value is undefined, don't try to set
-    if (value[0] !== undefined) {
-      var start = this._newDate(value[0]);
-      $inputs.eq(0).datepicker('update', start);
+  var textareaInputBinding = {};
+  $.extend(textareaInputBinding, textInputBinding, {
+    find: function find(scope) {
+      return $(scope).find('textarea');
     }
-    if (value[1] !== undefined) {
-      var end = this._newDate(value[1]);
-      $inputs.eq(1).datepicker('update', end);
+  });
+  inputBindings.register(textareaInputBinding, 'shiny.textareaInput');
+
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_password.js
+
+  var passwordInputBinding = {};
+  $.extend(passwordInputBinding, textInputBinding, {
+    find: function find(scope) {
+      return $(scope).find('input[type="password"]');
+    },
+    getType: function getType(el) {
+      return "shiny.password";
     }
-  },
-  getState: function(el) {
-    var $el = $(el);
-    var $inputs     = $el.find('input');
-    var $startinput = $inputs.eq(0);
-    var $endinput   = $inputs.eq(1);
-
-    // For many of the properties, assume start and end have the same values
-    var min = $startinput.data('datepicker').startDate;
-    var max = $startinput.data('datepicker').endDate;
-
-    // Stringify min and max. If min and max aren't set, they will be
-    // -Infinity and Infinity; replace these with null.
-    min = (min === -Infinity) ? null : formatDateUTC(min);
-    max = (max ===  Infinity) ? null : formatDateUTC(max);
-
-    // startViewMode is stored as a number; convert to string
-    var startview = $startinput.data('datepicker').startViewMode;
-    if      (startview === 2)  startview = 'decade';
-    else if (startview === 1)  startview = 'year';
-    else if (startview === 0)  startview = 'month';
+  });
+  inputBindings.register(passwordInputBinding, 'shiny.passwordInput');
 
-    return {
-      label:       $el.find('label[for="' + $escape(el.id) + '"]').text(),
-      value:       this.getValue(el),
-      valueString: [ $startinput.val(), $endinput.val() ],
-      min:         min,
-      max:         max,
-      weekstart:   $startinput.data('datepicker').weekStart,
-      format:      this._formatToString($startinput.data('datepicker').format),
-      language:    $startinput.data('datepicker').language,
-      startview:   startview
-    };
-  },
-  receiveMessage: function(el, data) {
-    var $el = $(el);
-    var $inputs     = $el.find('input');
-    var $startinput = $inputs.eq(0);
-    var $endinput   = $inputs.eq(1);
-
-    if (data.hasOwnProperty('value'))
-      this.setValue(el, data.value);
-
-    if (data.hasOwnProperty('label'))
-      $el.find('label[for="' + $escape(el.id) + '"]').text(data.label);
-
-    if (data.hasOwnProperty('min')) {
-      this._setMin($startinput[0], data.min);
-      this._setMin($endinput[0],   data.min);
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_number.js
+
+  var numberInputBinding = {};
+  $.extend(numberInputBinding, textInputBinding, {
+    find: function find(scope) {
+      return $(scope).find('input[type="number"]');
+    },
+    getValue: function getValue(el) {
+      var numberVal = $(el).val();
+      if (/^\s*$/.test(numberVal)) // Return null if all whitespace
+        return null;else if (!isNaN(numberVal)) // If valid Javascript number string, coerce to number
+        return +numberVal;else return numberVal; // If other string like "1e6", send it unchanged
+    },
+    setValue: function setValue(el, value) {
+      el.value = value;
+    },
+    getType: function getType(el) {
+      return "shiny.number";
+    },
+    receiveMessage: function receiveMessage(el, data) {
+      if (data.hasOwnProperty('value')) el.value = data.value;
+      if (data.hasOwnProperty('min')) el.min = data.min;
+      if (data.hasOwnProperty('max')) el.max = data.max;
+      if (data.hasOwnProperty('step')) el.step = data.step;
+
+      if (data.hasOwnProperty('label')) $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
+
+      $(el).trigger('change');
+    },
+    getState: function getState(el) {
+      return { label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
+        value: this.getValue(el),
+        min: Number(el.min),
+        max: Number(el.max),
+        step: Number(el.step) };
     }
+  });
+  inputBindings.register(numberInputBinding, 'shiny.numberInput');
+
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_checkbox.js
+
+  var checkboxInputBinding = new InputBinding();
+  $.extend(checkboxInputBinding, {
+    find: function find(scope) {
+      return $(scope).find('input[type="checkbox"]');
+    },
+    getValue: function getValue(el) {
+      return el.checked;
+    },
+    setValue: function setValue(el, value) {
+      el.checked = value;
+    },
+    subscribe: function subscribe(el, callback) {
+      $(el).on('change.checkboxInputBinding', function (event) {
+        callback(true);
+      });
+    },
+    unsubscribe: function unsubscribe(el) {
+      $(el).off('.checkboxInputBinding');
+    },
+    getState: function getState(el) {
+      return {
+        label: $(el).parent().find('span').text(),
+        value: el.checked
+      };
+    },
+    receiveMessage: function receiveMessage(el, data) {
+      if (data.hasOwnProperty('value')) el.checked = data.value;
+
+      if (data.hasOwnProperty('label')) $(el).parent().find('span').text(data.label);
 
-    if (data.hasOwnProperty('max')) {
-      this._setMax($startinput[0], data.max);
-      this._setMax($endinput[0],   data.max);
+      $(el).trigger('change');
     }
+  });
+  inputBindings.register(checkboxInputBinding, 'shiny.checkboxInput');
 
-    $el.trigger('change');
-  },
-  initialize: function(el) {
-    var $el = $(el);
-    var $inputs     = $el.find('input');
-    var $startinput = $inputs.eq(0);
-    var $endinput   = $inputs.eq(1);
-
-    var start = $startinput.data('initial-date');
-    var end   = $endinput.data('initial-date');
-
-    // If empty/null, use local date, but as UTC
-    if (start === undefined || start === null)
-      start = this._dateAsUTC(new Date());
-
-    if (end === undefined || end === null)
-      end = this._dateAsUTC(new Date());
-
-    this.setValue(el, [start, end]);
-
-    // // Set the start and end dates, from min-date and max-date. These always
-    // // use yyyy-mm-dd format, instead of bootstrap-datepicker's built-in
-    // // support for date-startdate and data-enddate, which use the current
-    // // date format.
-    this._setMin($startinput[0], $startinput.data('min-date'));
-    this._setMin($endinput[0],   $startinput.data('min-date'));
-    this._setMax($startinput[0], $endinput.data('max-date'));
-    this._setMax($endinput[0],   $endinput.data('max-date'));
-  },
-  subscribe: function(el, callback) {
-    $(el).on('keyup.dateRangeInputBinding input.dateRangeInputBinding', function(event) {
-      // Use normal debouncing policy when typing
-      callback(true);
-    });
-    $(el).on('changeDate.dateRangeInputBinding change.dateRangeInputBinding', function(event) {
-      // Send immediately when clicked
-      callback(false);
-    });
-  },
-  unsubscribe: function(el) {
-    $(el).off('.dateRangeInputBinding');
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_slider.js
+
+  // Necessary to get hidden sliders to send their updated values
+  function forceIonSliderUpdate(slider) {
+    if (slider.$cache && slider.$cache.input) slider.$cache.input.trigger('change');else console.log("Couldn't force ion slider to update");
   }
-});
-inputBindings.register(dateRangeInputBinding, 'shiny.dateRangeInput');
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_select.js
-
-var selectInputBinding = new InputBinding();
-$.extend(selectInputBinding, {
-  find: function(scope) {
-    return $(scope).find('select');
-  },
-  getId: function(el) {
-    return InputBinding.prototype.getId.call(this, el) || el.name;
-  },
-  getValue: function(el) {
-    return $(el).val();
-  },
-  setValue: function(el, value) {
-    var selectize = this._selectize(el);
-    if (typeof(selectize) !== 'undefined') {
-      selectize.setValue(value);
-    } else $(el).val(value);
-  },
-  getState: function(el) {
-    // Store options in an array of objects, each with with value and label
-    var options = new Array(el.length);
-    for (var i = 0; i < el.length; i++) {
-      options[i] = { value:    el[i].value,
-                     label:    el[i].label };
+  var sliderInputBinding = {};
+  $.extend(sliderInputBinding, textInputBinding, {
+    find: function find(scope) {
+      // Check if ionRangeSlider plugin is loaded
+      if (!$.fn.ionRangeSlider) return [];
+
+      return $(scope).find('input.js-range-slider');
+    },
+    getType: function getType(el) {
+      var dataType = $(el).data('data-type');
+      if (dataType === 'date') return 'shiny.date';else if (dataType === 'datetime') return 'shiny.datetime';else return false;
+    },
+    getValue: function getValue(el) {
+      var $el = $(el);
+      var result = $(el).data('ionRangeSlider').result;
+
+      // Function for converting numeric value from slider to appropriate type.
+      var convert;
+      var dataType = $el.data('data-type');
+      if (dataType === 'date') {
+        convert = function convert(val) {
+          return formatDateUTC(new Date(+val));
+        };
+      } else if (dataType === 'datetime') {
+        convert = function convert(val) {
+          // Convert ms to s
+          return +val / 1000;
+        };
+      } else {
+        convert = function convert(val) {
+          return +val;
+        };
+      }
+
+      if (this._numValues(el) === 2) {
+        return [convert(result.from), convert(result.to)];
+      } else {
+        return convert(result.from);
+      }
+    },
+    setValue: function setValue(el, value) {
+      var $el = $(el);
+      var slider = $el.data('ionRangeSlider');
+
+      $el.data('immediate', true);
+      try {
+        if (this._numValues(el) === 2 && value instanceof Array) {
+          slider.update({ from: value[0], to: value[1] });
+        } else {
+          slider.update({ from: value });
+        }
+
+        forceIonSliderUpdate(slider);
+      } finally {
+        $el.data('immediate', false);
+      }
+    },
+    subscribe: function subscribe(el, callback) {
+      $(el).on('change.sliderInputBinding', function (event) {
+        callback(!$(el).data('immediate') && !$(el).data('animating'));
+      });
+    },
+    unsubscribe: function unsubscribe(el) {
+      $(el).off('.sliderInputBinding');
+    },
+    receiveMessage: function receiveMessage(el, data) {
+      var $el = $(el);
+      var slider = $el.data('ionRangeSlider');
+      var msg = {};
+
+      if (data.hasOwnProperty('value')) {
+        if (this._numValues(el) === 2 && data.value instanceof Array) {
+          msg.from = data.value[0];
+          msg.to = data.value[1];
+        } else {
+          msg.from = data.value;
+        }
+      }
+      if (data.hasOwnProperty('min')) msg.min = data.min;
+      if (data.hasOwnProperty('max')) msg.max = data.max;
+      if (data.hasOwnProperty('step')) msg.step = data.step;
+
+      if (data.hasOwnProperty('label')) $el.parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
+
+      $el.data('immediate', true);
+      try {
+        slider.update(msg);
+        forceIonSliderUpdate(slider);
+      } finally {
+        $el.data('immediate', false);
+      }
+    },
+    getRatePolicy: function getRatePolicy() {
+      return {
+        policy: 'debounce',
+        delay: 250
+      };
+    },
+    getState: function getState(el) {},
+    initialize: function initialize(el) {
+      var opts = {};
+      var $el = $(el);
+      var dataType = $el.data('data-type');
+      var timeFormat = $el.data('time-format');
+      var timeFormatter;
+
+      // Set up formatting functions
+      if (dataType === 'date') {
+        timeFormatter = strftime.utc();
+        opts.prettify = function (num) {
+          return timeFormatter(timeFormat, new Date(num));
+        };
+      } else if (dataType === 'datetime') {
+        var timezone = $el.data('timezone');
+        if (timezone) timeFormatter = strftime.timezone(timezone);else timeFormatter = strftime;
+
+        opts.prettify = function (num) {
+          return timeFormatter(timeFormat, new Date(num));
+        };
+      }
+
+      $el.ionRangeSlider(opts);
+    },
+
+    // Number of values; 1 for single slider, 2 for range slider
+    _numValues: function _numValues(el) {
+      if ($(el).data('ionRangeSlider').options.type === 'double') return 2;else return 1;
     }
+  });
+  inputBindings.register(sliderInputBinding, 'shiny.sliderInput');
+
+  $(document).on('click', '.slider-animate-button', function (evt) {
+    evt.preventDefault();
+    var self = $(this);
+    var target = $('#' + $escape(self.attr('data-target-id')));
+    var startLabel = 'Play';
+    var stopLabel = 'Pause';
+    var loop = self.attr('data-loop') !== undefined && !/^\s*false\s*$/i.test(self.attr('data-loop'));
+    var animInterval = self.attr('data-interval');
+    if (isNaN(animInterval)) animInterval = 1500;else animInterval = +animInterval;
+
+    if (!target.data('animTimer')) {
+      var slider;
+      var timer;
+
+      // Separate code paths:
+      // Backward compatible code for old-style jsliders (Shiny <= 0.10.2.2),
+      // and new-style ionsliders.
+      if (target.hasClass('jslider')) {
+        slider = target.slider();
+
+        // If we're currently at the end, restart
+        if (!slider.canStepNext()) slider.resetToStart();
+
+        timer = setInterval(function () {
+          if (loop && !slider.canStepNext()) {
+            slider.resetToStart();
+          } else {
+            slider.stepNext();
+            if (!loop && !slider.canStepNext()) {
+              self.click(); // stop the animation
+            }
+          }
+        }, animInterval);
+      } else {
+        slider = target.data('ionRangeSlider');
+        // Single sliders have slider.options.type == "single", and only the
+        // `from` value is used. Double sliders have type == "double", and also
+        // use the `to` value for the right handle.
+        var sliderCanStep = function sliderCanStep() {
+          if (slider.options.type === "double") return slider.result.to < slider.result.max;else return slider.result.from < slider.result.max;
+        };
+        var sliderReset = function sliderReset() {
+          var val = { from: slider.result.min };
+          // Preserve the current spacing for double sliders
+          if (slider.options.type === "double") val.to = val.from + (slider.result.to - slider.result.from);
 
-    return {
-      label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
-      value:    this.getValue(el),
-      options:  options
-    };
-  },
-  receiveMessage: function(el, data) {
-    var $el = $(el), selectize;
-
-    // This will replace all the options
-    if (data.hasOwnProperty('options')) {
-      selectize = this._selectize(el);
-      // Must destroy selectize before appending new options, otherwise
-      // selectize will restore the original select
-      if (selectize) selectize.destroy();
-      // Clear existing options and add each new one
-      $el.empty().append(data.options);
-      this._selectize(el);
+          slider.update(val);
+          forceIonSliderUpdate(slider);
+        };
+        var sliderStep = function sliderStep() {
+          // Don't overshoot the end
+          var val = {
+            from: Math.min(slider.result.max, slider.result.from + slider.options.step)
+          };
+          if (slider.options.type === "double") val.to = Math.min(slider.result.max, slider.result.to + slider.options.step);
+
+          slider.update(val);
+          forceIonSliderUpdate(slider);
+        };
+
+        // If we're currently at the end, restart
+        if (!sliderCanStep()) sliderReset();
+
+        timer = setInterval(function () {
+          if (loop && !sliderCanStep()) {
+            sliderReset();
+          } else {
+            sliderStep();
+            if (!loop && !sliderCanStep()) {
+              self.click(); // stop the animation
+            }
+          }
+        }, animInterval);
+      }
+
+      target.data('animTimer', timer);
+      self.attr('title', stopLabel);
+      self.addClass('playing');
+      target.data('animating', true);
+    } else {
+      clearTimeout(target.data('animTimer'));
+      target.removeData('animTimer');
+      self.attr('title', startLabel);
+      self.removeClass('playing');
+      target.removeData('animating');
     }
+  });
+
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_date.js
+
+  var dateInputBinding = new InputBinding();
+  $.extend(dateInputBinding, {
+    find: function find(scope) {
+      return $(scope).find('.shiny-date-input');
+    },
+    getType: function getType(el) {
+      return "shiny.date";
+    },
+    // Return the date in an unambiguous format, yyyy-mm-dd (as opposed to a
+    // format like mm/dd/yyyy)
+    getValue: function getValue(el) {
+      var date = $(el).find('input').bsDatepicker('getUTCDate');
+      return formatDateUTC(date);
+    },
+    // value must be an unambiguous string like '2001-01-01', or a Date object.
+    setValue: function setValue(el, value) {
+      // R's NA, which is null here will remove current value
+      if (value === null) {
+        $(el).find('input').val('').bsDatepicker('update');
+        return;
+      }
+
+      var date = this._newDate(value);
+      // If date is invalid, do nothing
+      if (isNaN(date)) return;
+
+      $(el).find('input').bsDatepicker('setUTCDate', date);
+    },
+    getState: function getState(el) {
+      var $el = $(el);
+      var $input = $el.find('input');
 
-    // re-initialize selectize
-    if (data.hasOwnProperty('config')) {
-      $el.parent()
-         .find('script[data-for="' + $escape(el.id) + '"]')
-         .replaceWith(data.config);
-      this._selectize(el, true);
+      var min = $input.data('datepicker').startDate;
+      var max = $input.data('datepicker').endDate;
+
+      // Stringify min and max. If min and max aren't set, they will be
+      // -Infinity and Infinity; replace these with null.
+      min = min === -Infinity ? null : formatDateUTC(min);
+      max = max === Infinity ? null : formatDateUTC(max);
+
+      // startViewMode is stored as a number; convert to string
+      var startview = $input.data('datepicker').startViewMode;
+      if (startview === 2) startview = 'decade';else if (startview === 1) startview = 'year';else if (startview === 0) startview = 'month';
+
+      return {
+        label: $el.find('label[for="' + $escape(el.id) + '"]').text(),
+        value: this.getValue(el),
+        valueString: $input.val(),
+        min: min,
+        max: max,
+        language: $input.data('datepicker').language,
+        weekstart: $input.data('datepicker').weekStart,
+        format: this._formatToString($input.data('datepicker').format),
+        startview: startview
+      };
+    },
+    receiveMessage: function receiveMessage(el, data) {
+      var $input = $(el).find('input');
+
+      if (data.hasOwnProperty('label')) $(el).find('label[for="' + $escape(el.id) + '"]').text(data.label);
+
+      if (data.hasOwnProperty('min')) this._setMin($input[0], data.min);
+
+      if (data.hasOwnProperty('max')) this._setMax($input[0], data.max);
+
+      // Must set value only after min and max have been set. If new value is
+      // outside the bounds of the previous min/max, then the result will be a
+      // blank input.
+      if (data.hasOwnProperty('value')) this.setValue(el, data.value);
+
+      $(el).trigger('change');
+    },
+    subscribe: function subscribe(el, callback) {
+      $(el).on('keyup.dateInputBinding input.dateInputBinding', function (event) {
+        // Use normal debouncing policy when typing
+        callback(true);
+      });
+      $(el).on('changeDate.dateInputBinding change.dateInputBinding', function (event) {
+        // Send immediately when clicked
+        callback(false);
+      });
+    },
+    unsubscribe: function unsubscribe(el) {
+      $(el).off('.dateInputBinding');
+    },
+    getRatePolicy: function getRatePolicy() {
+      return {
+        policy: 'debounce',
+        delay: 250
+      };
+    },
+    initialize: function initialize(el) {
+      var $input = $(el).find('input');
+
+      var date = $input.data('initial-date');
+      // If initial_date is null, set to current date
+      if (date === undefined || date === null) {
+        // Get local date, but as UTC
+        date = this._dateAsUTC(new Date());
+      }
+
+      this.setValue(el, date);
+
+      // Set the start and end dates, from min-date and max-date. These always
+      // use yyyy-mm-dd format, instead of bootstrap-datepicker's built-in
+      // support for date-startdate and data-enddate, which use the current
+      // date format.
+      if ($input.data('min-date') !== undefined) {
+        this._setMin($input[0], $input.data('min-date'));
+      }
+      if ($input.data('max-date') !== undefined) {
+        this._setMax($input[0], $input.data('max-date'));
+      }
+    },
+    // Given a format object from a date picker, return a string
+    _formatToString: function _formatToString(format) {
+      // Format object has structure like:
+      // { parts: ['mm', 'dd', 'yy'], separators: ['', '/', '/' ,''] }
+      var str = '';
+      for (var i = 0; i < format.parts.length; i++) {
+        str += format.separators[i] + format.parts[i];
+      }
+      str += format.separators[i];
+      return str;
+    },
+    // Given an unambiguous date string or a Date object, set the min (start) date.
+    // null will unset. undefined will result in no change,
+    _setMin: function _setMin(el, date) {
+      if (date === undefined) return;
+      if (date === null) {
+        $(el).bsDatepicker('setStartDate', null);
+      } else {
+        date = this._newDate(date);
+        date = this._UTCDateAsLocal(date);
+        if (!isNaN(date)) {
+          // Workaround for https://github.com/eternicode/bootstrap-datepicker/issues/2010
+          // If the start date when there's a two-digit year format, it will set
+          // the date value to null. So we'll save the value, set the start
+          // date, and the restore the value.
+          var curValue = $(el).bsDatepicker('getUTCDate');
+          $(el).bsDatepicker('setStartDate', date);
+          $(el).bsDatepicker('setUTCDate', curValue);
+        }
+      }
+    },
+    // Given an unambiguous date string or a Date object, set the max (end) date
+    // null will unset.
+    _setMax: function _setMax(el, date) {
+      if (date === undefined) return;
+      if (date === null) {
+        $(el).bsDatepicker('setEndDate', null);
+      } else {
+        date = this._newDate(date);
+        date = this._UTCDateAsLocal(date);
+        if (!isNaN(date)) {
+          // Workaround for same issue as in _setMin.
+          var curValue = $(el).bsDatepicker('getUTCDate');
+          $(el).bsDatepicker('setEndDate', date);
+          $(el).bsDatepicker('setUTCDate', curValue);
+        }
+      }
+    },
+    // Given a date string of format yyyy-mm-dd, return a Date object with
+    // that date at 12AM UTC.
+    // If date is a Date object, return it unchanged.
+    _newDate: function _newDate(date) {
+      if (date instanceof Date) return date;
+      if (!date) return null;
+
+      // Get Date object - this will be at 12AM in UTC, but may print
+      // differently at the Javascript console.
+      var d = parseDate(date);
+
+      // If invalid date, return null
+      if (isNaN(d)) return null;
+
+      return new Date(d.getTime());
+    },
+    // Given a Date object, return a Date object which has the same "clock time"
+    // in UTC. For example, if input date is 2013-02-01 23:00:00 GMT-0600 (CST),
+    // output will be 2013-02-01 23:00:00 UTC. Note that the JS console may
+    // print this in local time, as "Sat Feb 02 2013 05:00:00 GMT-0600 (CST)".
+    _dateAsUTC: function _dateAsUTC(date) {
+      return new Date(date.getTime() - date.getTimezoneOffset() * 60000);
+    },
+    // The inverse of _dateAsUTC. This is needed to adjust time zones because
+    // some bootstrap-datepicker methods only take local dates as input, and not
+    // UTC.
+    _UTCDateAsLocal: function _UTCDateAsLocal(date) {
+      return new Date(date.getTime() + date.getTimezoneOffset() * 60000);
     }
+  });
+  inputBindings.register(dateInputBinding, 'shiny.dateInput');
 
-    // use server-side processing for selectize
-    if (data.hasOwnProperty('url')) {
-      selectize = this._selectize(el);
-      selectize.clearOptions();
-      var thiz = this, loaded = false;
-      selectize.settings.load = function(query, callback) {
-        var settings = selectize.settings;
-        $.ajax({
-          url: data.url,
-          data: {
-            query: query,
-            field: JSON.stringify([settings.searchField]),
-            value: settings.valueField,
-            conju: settings.searchConjunction,
-            maxop: settings.maxOptions
-          },
-          type: 'GET',
-          error: function() {
-            callback();
-          },
-          success: function(res) {
-            callback(res);
-            if (!loaded && data.hasOwnProperty('value'))
-              thiz.setValue(el, data.value);
-            loaded = true;
-          }
-        });
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_daterange.js
+
+  var dateRangeInputBinding = {};
+  $.extend(dateRangeInputBinding, dateInputBinding, {
+    find: function find(scope) {
+      return $(scope).find('.shiny-date-range-input');
+    },
+    // Return the date in an unambiguous format, yyyy-mm-dd (as opposed to a
+    // format like mm/dd/yyyy)
+    getValue: function getValue(el) {
+      var $inputs = $(el).find('input');
+      var start = $inputs.eq(0).bsDatepicker('getUTCDate');
+      var end = $inputs.eq(1).bsDatepicker('getUTCDate');
+
+      return [formatDateUTC(start), formatDateUTC(end)];
+    },
+    // value must be an object, with optional fields `start` and `end`. These
+    // should be unambiguous strings like '2001-01-01', or Date objects.
+    setValue: function setValue(el, value) {
+      if (!(value instanceof Object)) {
+        return;
+      }
+
+      // Get the start and end input objects
+      var $inputs = $(el).find('input');
+
+      // If value is undefined, don't try to set
+      // null will remove the current value
+      if (value.start !== undefined) {
+        if (value.start === null) {
+          $inputs.eq(0).val('').bsDatepicker('update');
+        } else {
+          var start = this._newDate(value.start);
+          $inputs.eq(0).bsDatepicker('setUTCDate', start);
+        }
+      }
+      if (value.end !== undefined) {
+        if (value.end === null) {
+          $inputs.eq(1).val('').bsDatepicker('update');
+        } else {
+          var end = this._newDate(value.end);
+          $inputs.eq(1).bsDatepicker('setUTCDate', end);
+        }
+      }
+    },
+    getState: function getState(el) {
+      var $el = $(el);
+      var $inputs = $el.find('input');
+      var $startinput = $inputs.eq(0);
+      var $endinput = $inputs.eq(1);
+
+      // For many of the properties, assume start and end have the same values
+      var min = $startinput.bsDatepicker('getStartDate');
+      var max = $startinput.bsDatepicker('getEndDate');
+
+      // Stringify min and max. If min and max aren't set, they will be
+      // -Infinity and Infinity; replace these with null.
+      min = min === -Infinity ? null : formatDateUTC(min);
+      max = max === Infinity ? null : formatDateUTC(max);
+
+      // startViewMode is stored as a number; convert to string
+      var startview = $startinput.data('datepicker').startView;
+      if (startview === 2) startview = 'decade';else if (startview === 1) startview = 'year';else if (startview === 0) startview = 'month';
+
+      return {
+        label: $el.find('label[for="' + $escape(el.id) + '"]').text(),
+        value: this.getValue(el),
+        valueString: [$startinput.val(), $endinput.val()],
+        min: min,
+        max: max,
+        weekstart: $startinput.data('datepicker').weekStart,
+        format: this._formatToString($startinput.data('datepicker').format),
+        language: $startinput.data('datepicker').language,
+        startview: startview
       };
-      // perform an empty search after changing the `load` function
-      selectize.load(function(callback) {
-        selectize.settings.load.apply(selectize, ['', callback]);
+    },
+    receiveMessage: function receiveMessage(el, data) {
+      var $el = $(el);
+      var $inputs = $el.find('input');
+      var $startinput = $inputs.eq(0);
+      var $endinput = $inputs.eq(1);
+
+      if (data.hasOwnProperty('label')) $el.find('label[for="' + $escape(el.id) + '"]').text(data.label);
+
+      if (data.hasOwnProperty('min')) {
+        this._setMin($startinput[0], data.min);
+        this._setMin($endinput[0], data.min);
+      }
+
+      if (data.hasOwnProperty('max')) {
+        this._setMax($startinput[0], data.max);
+        this._setMax($endinput[0], data.max);
+      }
+
+      // Must set value only after min and max have been set. If new value is
+      // outside the bounds of the previous min/max, then the result will be a
+      // blank input.
+      if (data.hasOwnProperty('value')) this.setValue(el, data.value);
+
+      $el.trigger('change');
+    },
+    initialize: function initialize(el) {
+      var $el = $(el);
+      var $inputs = $el.find('input');
+      var $startinput = $inputs.eq(0);
+      var $endinput = $inputs.eq(1);
+
+      var start = $startinput.data('initial-date');
+      var end = $endinput.data('initial-date');
+
+      // If empty/null, use local date, but as UTC
+      if (start === undefined || start === null) start = this._dateAsUTC(new Date());
+
+      if (end === undefined || end === null) end = this._dateAsUTC(new Date());
+
+      this.setValue(el, { "start": start, "end": end });
+
+      // // Set the start and end dates, from min-date and max-date. These always
+      // // use yyyy-mm-dd format, instead of bootstrap-datepicker's built-in
+      // // support for date-startdate and data-enddate, which use the current
+      // // date format.
+      this._setMin($startinput[0], $startinput.data('min-date'));
+      this._setMin($endinput[0], $startinput.data('min-date'));
+      this._setMax($startinput[0], $endinput.data('max-date'));
+      this._setMax($endinput[0], $endinput.data('max-date'));
+    },
+    subscribe: function subscribe(el, callback) {
+      $(el).on('keyup.dateRangeInputBinding input.dateRangeInputBinding', function (event) {
+        // Use normal debouncing policy when typing
+        callback(true);
       });
-    } else if (data.hasOwnProperty('value')) {
-      this.setValue(el, data.value);
+      $(el).on('changeDate.dateRangeInputBinding change.dateRangeInputBinding', function (event) {
+        // Send immediately when clicked
+        callback(false);
+      });
+    },
+    unsubscribe: function unsubscribe(el) {
+      $(el).off('.dateRangeInputBinding');
     }
+  });
+  inputBindings.register(dateRangeInputBinding, 'shiny.dateRangeInput');
 
-    if (data.hasOwnProperty('label'))
-      $(el).parent().parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_select.js
 
-    $(el).trigger('change');
-  },
-  subscribe: function(el, callback) {
-    $(el).on('change.selectInputBinding', function(event) {
-      callback();
-    });
-  },
-  unsubscribe: function(el) {
-    $(el).off('.selectInputBinding');
-  },
-  initialize: function(el) {
-    this._selectize(el);
-  },
-  _selectize: function(el, update) {
-    if (!$.fn.selectize) return;
-    var $el = $(el);
-    var config = $el.parent().find('script[data-for="' + $escape(el.id) + '"]');
-    if (config.length === 0) return;
-    var options = $.extend({
-      labelField: 'label',
-      valueField: 'value',
-      searchField: ['label']
-    }, JSON.parse(config.html()));
-    // selectize created from selectInput()
-    if (typeof(config.data('nonempty')) !== 'undefined') {
-      options = $.extend(options, {
-        onItemRemove: function(value) {
-          if (this.getValue() === "")
-            $("select#" + $escape(el.id)).empty().append($("<option/>", {
+  var selectInputBinding = new InputBinding();
+  $.extend(selectInputBinding, {
+    find: function find(scope) {
+      return $(scope).find('select');
+    },
+    getId: function getId(el) {
+      return InputBinding.prototype.getId.call(this, el) || el.name;
+    },
+    getValue: function getValue(el) {
+      return $(el).val();
+    },
+    setValue: function setValue(el, value) {
+      var selectize = this._selectize(el);
+      if (typeof selectize !== 'undefined') {
+        selectize.setValue(value);
+      } else $(el).val(value);
+    },
+    getState: function getState(el) {
+      // Store options in an array of objects, each with with value and label
+      var options = new Array(el.length);
+      for (var i = 0; i < el.length; i++) {
+        options[i] = { value: el[i].value,
+          label: el[i].label };
+      }
+
+      return {
+        label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
+        value: this.getValue(el),
+        options: options
+      };
+    },
+    receiveMessage: function receiveMessage(el, data) {
+      var $el = $(el),
+          selectize;
+
+      // This will replace all the options
+      if (data.hasOwnProperty('options')) {
+        selectize = this._selectize(el);
+        // Must destroy selectize before appending new options, otherwise
+        // selectize will restore the original select
+        if (selectize) selectize.destroy();
+        // Clear existing options and add each new one
+        $el.empty().append(data.options);
+        this._selectize(el);
+      }
+
+      // re-initialize selectize
+      if (data.hasOwnProperty('config')) {
+        $el.parent().find('script[data-for="' + $escape(el.id) + '"]').replaceWith(data.config);
+        this._selectize(el, true);
+      }
+
+      // use server-side processing for selectize
+      if (data.hasOwnProperty('url')) {
+        selectize = this._selectize(el);
+        selectize.clearOptions();
+        var thiz = this,
+            loaded = false;
+        selectize.settings.load = function (query, callback) {
+          var settings = selectize.settings;
+          $.ajax({
+            url: data.url,
+            data: {
+              query: query,
+              field: JSON.stringify([settings.searchField]),
+              value: settings.valueField,
+              conju: settings.searchConjunction,
+              maxop: settings.maxOptions
+            },
+            type: 'GET',
+            error: function error() {
+              callback();
+            },
+            success: function success(res) {
+              callback(res);
+              if (!loaded && data.hasOwnProperty('value')) thiz.setValue(el, data.value);
+              loaded = true;
+            }
+          });
+        };
+        // perform an empty search after changing the `load` function
+        selectize.load(function (callback) {
+          selectize.settings.load.apply(selectize, ['', callback]);
+        });
+      } else if (data.hasOwnProperty('value')) {
+        this.setValue(el, data.value);
+      }
+
+      if (data.hasOwnProperty('label')) $(el).parent().parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
+
+      $(el).trigger('change');
+    },
+    subscribe: function subscribe(el, callback) {
+      $(el).on('change.selectInputBinding', function (event) {
+        callback();
+      });
+    },
+    unsubscribe: function unsubscribe(el) {
+      $(el).off('.selectInputBinding');
+    },
+    initialize: function initialize(el) {
+      this._selectize(el);
+    },
+    _selectize: function _selectize(el, update) {
+      if (!$.fn.selectize) return undefined;
+      var $el = $(el);
+      var config = $el.parent().find('script[data-for="' + $escape(el.id) + '"]');
+      if (config.length === 0) return undefined;
+      var options = $.extend({
+        labelField: 'label',
+        valueField: 'value',
+        searchField: ['label']
+      }, JSON.parse(config.html()));
+      // selectize created from selectInput()
+      if (typeof config.data('nonempty') !== 'undefined') {
+        options = $.extend(options, {
+          onItemRemove: function onItemRemove(value) {
+            if (this.getValue() === "") $("select#" + $escape(el.id)).empty().append($("<option/>", {
               "value": value,
               "selected": true
             })).trigger("change");
-        },
-        onDropdownClose: function($dropdown) {
-          if (this.getValue() === "")
-            this.setValue($("select#" + $escape(el.id)).val());
-        }
-      });
-    }
-    // options that should be eval()ed
-    if (config.data('eval') instanceof Array)
-      $.each(config.data('eval'), function(i, x) {
+          },
+          onDropdownClose: function onDropdownClose($dropdown) {
+            if (this.getValue() === "") this.setValue($("select#" + $escape(el.id)).val());
+          }
+        });
+      }
+      // options that should be eval()ed
+      if (config.data('eval') instanceof Array) $.each(config.data('eval'), function (i, x) {
         /*jshint evil: true*/
         options[x] = eval('(' + options[x] + ')');
       });
-    var control = $el.selectize(options)[0].selectize;
-    // .selectize() does not really update settings; must destroy and rebuild
-    if (update) {
-      var settings = $.extend(control.settings, options);
-      control.destroy();
-      control = $el.selectize(settings)[0].selectize;
+      var control = $el.selectize(options)[0].selectize;
+      // .selectize() does not really update settings; must destroy and rebuild
+      if (update) {
+        var settings = $.extend(control.settings, options);
+        control.destroy();
+        control = $el.selectize(settings)[0].selectize;
+      }
+      return control;
     }
-    return control;
-  }
-});
-inputBindings.register(selectInputBinding, 'shiny.selectInput');
+  });
+  inputBindings.register(selectInputBinding, 'shiny.selectInput');
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_radio.js
-
-var radioInputBinding = new InputBinding();
-$.extend(radioInputBinding, {
-  find: function(scope) {
-    return $(scope).find('.shiny-input-radiogroup');
-  },
-  getValue: function(el) {
-    // Select the radio objects that have name equal to the grouping div's id
-    return $('input:radio[name="' + $escape(el.id) + '"]:checked').val();
-  },
-  setValue: function(el, value) {
-    $('input:radio[name="' + $escape(el.id) + '"][value="' + $escape(value) + '"]').prop('checked', true);
-  },
-  getState: function(el) {
-    var $objs = $('input:radio[name="' + $escape(el.id) + '"]');
-
-    // Store options in an array of objects, each with with value and label
-    var options = new Array($objs.length);
-    for (var i = 0; i < options.length; i++) {
-      options[i] = { value:   $objs[i].value,
-                     label:   this._getLabel($objs[i]) };
-    }
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_radio.js
 
-    return {
-      label:    $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
-      value:    this.getValue(el),
-      options:  options
-    };
-  },
-  receiveMessage: function(el, data) {
-    var $el = $(el);
-
-    // This will replace all the options
-    if (data.hasOwnProperty('options')) {
-      // Clear existing options and add each new one
-      $el.find('div.shiny-options-group').remove();
-      // Backward compatibility: for HTML generated by shinybootstrap2 package
-      $el.find('label.radio').remove();
-      $el.append(data.options);
+  var radioInputBinding = new InputBinding();
+  $.extend(radioInputBinding, {
+    find: function find(scope) {
+      return $(scope).find('.shiny-input-radiogroup');
+    },
+    getValue: function getValue(el) {
+      // Select the radio objects that have name equal to the grouping div's id
+      return $('input:radio[name="' + $escape(el.id) + '"]:checked').val();
+    },
+    setValue: function setValue(el, value) {
+      $('input:radio[name="' + $escape(el.id) + '"][value="' + $escape(value) + '"]').prop('checked', true);
+    },
+    getState: function getState(el) {
+      var $objs = $('input:radio[name="' + $escape(el.id) + '"]');
+
+      // Store options in an array of objects, each with with value and label
+      var options = new Array($objs.length);
+      for (var i = 0; i < options.length; i++) {
+        options[i] = { value: $objs[i].value,
+          label: this._getLabel($objs[i]) };
+      }
+
+      return {
+        label: $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(),
+        value: this.getValue(el),
+        options: options
+      };
+    },
+    receiveMessage: function receiveMessage(el, data) {
+      var $el = $(el);
+
+      // This will replace all the options
+      if (data.hasOwnProperty('options')) {
+        // Clear existing options and add each new one
+        $el.find('div.shiny-options-group').remove();
+        // Backward compatibility: for HTML generated by shinybootstrap2 package
+        $el.find('label.radio').remove();
+        $el.append(data.options);
+      }
+
+      if (data.hasOwnProperty('value')) this.setValue(el, data.value);
+
+      if (data.hasOwnProperty('label')) $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
+
+      $(el).trigger('change');
+    },
+    subscribe: function subscribe(el, callback) {
+      $(el).on('change.radioInputBinding', function (event) {
+        callback();
+      });
+    },
+    unsubscribe: function unsubscribe(el) {
+      $(el).off('.radioInputBinding');
+    },
+    // Given an input DOM object, get the associated label. Handles labels
+    // that wrap the input as well as labels associated with 'for' attribute.
+    _getLabel: function _getLabel(obj) {
+      // If <label><input /><span>label text</span></label>
+      if (obj.parentNode.tagName === "LABEL") {
+        return $.trim($(obj.parentNode).find('span').text());
+      }
+
+      return null;
+    },
+    // Given an input DOM object, set the associated label. Handles labels
+    // that wrap the input as well as labels associated with 'for' attribute.
+    _setLabel: function _setLabel(obj, value) {
+      // If <label><input /><span>label text</span></label>
+      if (obj.parentNode.tagName === "LABEL") {
+        $(obj.parentNode).find('span').text(value);
+      }
+
+      return null;
     }
 
-    if (data.hasOwnProperty('value'))
-      this.setValue(el, data.value);
+  });
+  inputBindings.register(radioInputBinding, 'shiny.radioInput');
+
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_checkboxgroup.js
+
+  var checkboxGroupInputBinding = new InputBinding();
+  $.extend(checkboxGroupInputBinding, {
+    find: function find(scope) {
+      return $(scope).find('.shiny-input-checkboxgroup');
+    },
+    getValue: function getValue(el) {
+      // Select the checkbox objects that have name equal to the grouping div's id
+      var $objs = $('input:checkbox[name="' + $escape(el.id) + '"]:checked');
+      var values = new Array($objs.length);
+      for (var i = 0; i < $objs.length; i++) {
+        values[i] = $objs[i].value;
+      }
+      return values;
+    },
+    setValue: function setValue(el, value) {
+      // Clear all checkboxes
+      $('input:checkbox[name="' + $escape(el.id) + '"]').prop('checked', false);
+
+      // Accept array
+      if (value instanceof Array) {
+        for (var i = 0; i < value.length; i++) {
+          $('input:checkbox[name="' + $escape(el.id) + '"][value="' + $escape(value[i]) + '"]').prop('checked', true);
+        }
+        // Else assume it's a single value
+      } else {
+        $('input:checkbox[name="' + $escape(el.id) + '"][value="' + $escape(value) + '"]').prop('checked', true);
+      }
+    },
+    getState: function getState(el) {
+      var $objs = $('input:checkbox[name="' + $escape(el.id) + '"]');
+
+      // Store options in an array of objects, each with with value and label
+      var options = new Array($objs.length);
+      for (var i = 0; i < options.length; i++) {
+        options[i] = { value: $objs[i].value,
+          label: this._getLabel($objs[i]) };
+      }
 
-    if (data.hasOwnProperty('label'))
-      $(el).parent().find('label[for="' + $escape(el.id) + '"]').text(data.label);
+      return { label: $(el).find('label[for="' + $escape(el.id) + '"]').text(),
+        value: this.getValue(el),
+        options: options
+      };
+    },
+    receiveMessage: function receiveMessage(el, data) {
+      var $el = $(el);
+
+      // This will replace all the options
+      if (data.hasOwnProperty('options')) {
+        // Clear existing options and add each new one
+        $el.find('div.shiny-options-group').remove();
+        // Backward compatibility: for HTML generated by shinybootstrap2 package
+        $el.find('label.checkbox').remove();
+        $el.append(data.options);
+      }
 
-    $(el).trigger('change');
-  },
-  subscribe: function(el, callback) {
-    $(el).on('change.radioInputBinding', function(event) {
-      callback();
-    });
-  },
-  unsubscribe: function(el) {
-    $(el).off('.radioInputBinding');
-  },
-  // Given an input DOM object, get the associated label. Handles labels
-  // that wrap the input as well as labels associated with 'for' attribute.
-  _getLabel: function(obj) {
-    // If <label><input /><span>label text</span></label>
-    if (obj.parentNode.tagName === "LABEL") {
-      return $.trim($(obj.parentNode).find('span').text());
-    }
+      if (data.hasOwnProperty('value')) this.setValue(el, data.value);
 
-    return null;
-  },
-  // Given an input DOM object, set the associated label. Handles labels
-  // that wrap the input as well as labels associated with 'for' attribute.
-  _setLabel: function(obj, value) {
-    // If <label><input /><span>label text</span></label>
-    if (obj.parentNode.tagName === "LABEL") {
-      $(obj.parentNode).find('span').text(value);
-    }
+      if (data.hasOwnProperty('label')) $el.find('label[for="' + $escape(el.id) + '"]').text(data.label);
 
-    return null;
-  }
+      $(el).trigger('change');
+    },
+    subscribe: function subscribe(el, callback) {
+      $(el).on('change.checkboxGroupInputBinding', function (event) {
+        callback();
+      });
+    },
+    unsubscribe: function unsubscribe(el) {
+      $(el).off('.checkboxGroupInputBinding');
+    },
+    // Given an input DOM object, get the associated label. Handles labels
+    // that wrap the input as well as labels associated with 'for' attribute.
+    _getLabel: function _getLabel(obj) {
+      // If <label><input /><span>label text</span></label>
+      if (obj.parentNode.tagName === "LABEL") {
+        return $.trim($(obj.parentNode).find('span').text());
+      }
 
-});
-inputBindings.register(radioInputBinding, 'shiny.radioInput');
+      return null;
+    },
+    // Given an input DOM object, set the associated label. Handles labels
+    // that wrap the input as well as labels associated with 'for' attribute.
+    _setLabel: function _setLabel(obj, value) {
+      // If <label><input /><span>label text</span></label>
+      if (obj.parentNode.tagName === "LABEL") {
+        $(obj.parentNode).find('span').text(value);
+      }
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_checkboxgroup.js
-
-var checkboxGroupInputBinding = new InputBinding();
-$.extend(checkboxGroupInputBinding, {
-  find: function(scope) {
-    return $(scope).find('.shiny-input-checkboxgroup');
-  },
-  getValue: function(el) {
-    // Select the checkbox objects that have name equal to the grouping div's id
-    var $objs = $('input:checkbox[name="' + $escape(el.id) + '"]:checked');
-    var values = new Array($objs.length);
-    for (var i = 0; i < $objs.length; i ++) {
-      values[i] = $objs[i].value;
-    }
-    return values;
-  },
-  setValue: function(el, value) {
-    // Clear all checkboxes
-    $('input:checkbox[name="' + $escape(el.id) + '"]').prop('checked', false);
-
-    // Accept array
-    if (value instanceof Array) {
-      for (var i = 0; i < value.length; i++) {
-        $('input:checkbox[name="' + $escape(el.id) + '"][value="' + $escape(value[i]) + '"]')
-          .prop('checked', true);
-      }
-    // Else assume it's a single value
-    } else {
-      $('input:checkbox[name="' + $escape(el.id) + '"][value="' + $escape(value) + '"]')
-        .prop('checked', true);
+      return null;
     }
 
-  },
-  getState: function(el) {
-    var $objs = $('input:checkbox[name="' + $escape(el.id) + '"]');
-
-    // Store options in an array of objects, each with with value and label
-    var options = new Array($objs.length);
-    for (var i = 0; i < options.length; i++) {
-      options[i] = { value:   $objs[i].value,
-                     label:   this._getLabel($objs[i]) };
-    }
+  });
+  inputBindings.register(checkboxGroupInputBinding, 'shiny.checkboxGroupInput');
 
-    return { label:    $(el).find('label[for="' + $escape(el.id) + '"]').text(),
-             value:    this.getValue(el),
-             options:  options
-           };
-  },
-  receiveMessage: function(el, data) {
-    var $el = $(el);
-
-    // This will replace all the options
-    if (data.hasOwnProperty('options')) {
-      // Clear existing options and add each new one
-      $el.find('div.shiny-options-group').remove();
-      // Backward compatibility: for HTML generated by shinybootstrap2 package
-      $el.find('label.checkbox').remove();
-      $el.append(data.options);
-    }
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_actionbutton.js
 
-    if (data.hasOwnProperty('value'))
-      this.setValue(el, data.value);
+  var actionButtonInputBinding = new InputBinding();
+  $.extend(actionButtonInputBinding, {
+    find: function find(scope) {
+      return $(scope).find(".action-button");
+    },
+    getValue: function getValue(el) {
+      return $(el).data('val') || 0;
+    },
+    setValue: function setValue(el, value) {
+      $(el).data('val', value);
+    },
+    getType: function getType(el) {
+      return 'shiny.action';
+    },
+    subscribe: function subscribe(el, callback) {
+      $(el).on("click.actionButtonInputBinding", function (e) {
+        var $el = $(this);
+        var val = $el.data('val') || 0;
+        $el.data('val', val + 1);
 
-    if (data.hasOwnProperty('label'))
-      $el.find('label[for="' + $escape(el.id) + '"]').text(data.label);
+        callback();
+      });
+    },
+    getState: function getState(el) {
+      return { value: this.getValue(el) };
+    },
+    receiveMessage: function receiveMessage(el, data) {
+      var $el = $(el);
+
+      // retrieve current label and icon
+      var label = $el.text();
+      var icon = '';
+
+      // to check (and store) the previous icon, we look for a $el child
+      // object that has an i tag, and some (any) class (this prevents
+      // italicized text - which has an i tag but, usually, no class -
+      // from being mistakenly selected)
+      if ($el.find('i[class]').length > 0) {
+        var icon_html = $el.find('i[class]')[0];
+        if (icon_html === $el.children()[0]) {
+          // another check for robustness
+          icon = $(icon_html).prop('outerHTML');
+        }
+      }
 
-    $(el).trigger('change');
-  },
-  subscribe: function(el, callback) {
-    $(el).on('change.checkboxGroupInputBinding', function(event) {
-      callback();
-    });
-  },
-  unsubscribe: function(el) {
-    $(el).off('.checkboxGroupInputBinding');
-  },
-  // Given an input DOM object, get the associated label. Handles labels
-  // that wrap the input as well as labels associated with 'for' attribute.
-  _getLabel: function(obj) {
-    // If <label><input /><span>label text</span></label>
-    if (obj.parentNode.tagName === "LABEL") {
-      return $.trim($(obj.parentNode).find('span').text());
-    }
+      // update the requested properties
+      if (data.hasOwnProperty('label')) label = data.label;
+      if (data.hasOwnProperty('icon')) {
+        icon = data.icon;
+        // if the user entered icon=character(0), remove the icon
+        if (icon.length === 0) icon = '';
+      }
 
-    return null;
-  },
-  // Given an input DOM object, set the associated label. Handles labels
-  // that wrap the input as well as labels associated with 'for' attribute.
-  _setLabel: function(obj, value) {
-    // If <label><input /><span>label text</span></label>
-    if (obj.parentNode.tagName === "LABEL") {
-      $(obj.parentNode).find('span').text(value);
+      // produce new html
+      $el.html(icon + ' ' + label);
+    },
+    unsubscribe: function unsubscribe(el) {
+      $(el).off(".actionButtonInputBinding");
     }
+  });
+  inputBindings.register(actionButtonInputBinding, 'shiny.actionButtonInput');
 
-    return null;
-  }
+  $(document).on('click', 'a.action-button', function (e) {
+    e.preventDefault();
+  });
 
-});
-inputBindings.register(checkboxGroupInputBinding, 'shiny.checkboxGroupInput');
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_tabinput.js
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_actionbutton.js
-
-var actionButtonInputBinding = new InputBinding();
-$.extend(actionButtonInputBinding, {
-  find: function(scope) {
-    return $(scope).find(".action-button");
-  },
-  getValue: function(el) {
-    return $(el).data('val') || 0;
-  },
-  setValue: function(el, value) {
-    $(el).data('val', value);
-  },
-  getType: function(el) {
-    return 'shiny.action';
-  },
-  subscribe: function(el, callback) {
-    $(el).on("click.actionButtonInputBinding", function(e) {
-      var $el = $(this);
-      var val = $el.data('val') || 0;
-      $el.data('val', val + 1);
-
-      callback();
-    });
-  },
-  getState: function(el) {
-    return { value: this.getValue(el) };
-  },
-  receiveMessage: function(el, data) {
-  },
-  unsubscribe: function(el) {
-    $(el).off(".actionButtonInputBinding");
-  }
-});
-inputBindings.register(actionButtonInputBinding, 'shiny.actionButtonInput');
+  var bootstrapTabInputBinding = new InputBinding();
+  $.extend(bootstrapTabInputBinding, {
+    find: function find(scope) {
+      return $(scope).find('ul.nav.shiny-tab-input');
+    },
+    getValue: function getValue(el) {
+      var anchor = $(el).find('li:not(.dropdown).active').children('a');
+      if (anchor.length === 1) return this._getTabName(anchor);
 
+      return null;
+    },
+    setValue: function setValue(el, value) {
+      var self = this;
+      var anchors = $(el).find('li:not(.dropdown)').children('a');
+      anchors.each(function () {
+        if (self._getTabName($(this)) === value) {
+          $(this).tab('show');
+          return false; // Break out of each()
+        }
+        return true;
+      });
+    },
+    getState: function getState(el) {
+      return { value: this.getValue(el) };
+    },
+    receiveMessage: function receiveMessage(el, data) {
+      if (data.hasOwnProperty('value')) this.setValue(el, data.value);
+    },
+    subscribe: function subscribe(el, callback) {
+      $(el).on('shown.bootstrapTabInputBinding shown.bs.tab.bootstrapTabInputBinding', function (event) {
+        callback();
+      });
+    },
+    unsubscribe: function unsubscribe(el) {
+      $(el).off('.bootstrapTabInputBinding');
+    },
+    _getTabName: function _getTabName(anchor) {
+      return anchor.attr('data-value') || anchor.text();
+    }
+  });
+  inputBindings.register(bootstrapTabInputBinding, 'shiny.bootstrapTabInput');
 
-$(document).on('click', 'a.action-button', function(e) {
-  e.preventDefault();
-});
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/input_binding_fileinput.js
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_tabinput.js
-
-var bootstrapTabInputBinding = new InputBinding();
-$.extend(bootstrapTabInputBinding, {
-  find: function(scope) {
-    return $(scope).find('ul.nav.shiny-tab-input');
-  },
-  getValue: function(el) {
-    var anchor = $(el).find('li:not(.dropdown).active').children('a');
-    if (anchor.length === 1)
-      return this._getTabName(anchor);
-
-    return null;
-  },
-  setValue: function(el, value) {
-    var self = this;
-    var anchors = $(el).find('li:not(.dropdown)').children('a');
-    anchors.each(function() {
-      if (self._getTabName($(this)) === value) {
-        $(this).tab('show');
-        return false;
+  var IE8FileUploader = function IE8FileUploader(shinyapp, id, fileEl) {
+    this.shinyapp = shinyapp;
+    this.id = id;
+    this.fileEl = fileEl;
+    this.beginUpload();
+  };
+  (function () {
+    this.beginUpload = function () {
+      var self = this;
+      // Create invisible frame
+      var iframeId = 'shinyupload_iframe_' + this.id;
+      this.iframe = document.createElement('iframe');
+      this.iframe.id = iframeId;
+      this.iframe.name = iframeId;
+      this.iframe.setAttribute('style', 'position: fixed; top: 0; left: 0; width: 0; height: 0; border: none');
+      $('body').append(this.iframe);
+      var iframeDestroy = function iframeDestroy() {
+        // Forces Shiny to flushReact, flush outputs, etc. Without this we get
+        // invalidated reactives, but observers don't actually execute.
+        self.shinyapp.makeRequest('uploadieFinish', [], function () {}, function () {});
+        $(self.iframe).remove();
+      };
+      if (this.iframe.attachEvent) {
+        this.iframe.attachEvent('onload', iframeDestroy);
+      } else {
+        this.iframe.onload = iframeDestroy;
       }
-    });
-  },
-  getState: function(el) {
-    return { value: this.getValue(el) };
-  },
-  receiveMessage: function(el, data) {
-    if (data.hasOwnProperty('value'))
-      this.setValue(el, data.value);
-  },
-  subscribe: function(el, callback) {
-    $(el).on('shown.bootstrapTabInputBinding shown.bs.tab.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');
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/input_binding_fileinput.js
-
-var IE8FileUploader = function(shinyapp, id, fileEl) {
-  this.shinyapp = shinyapp;
-  this.id = id;
-  this.fileEl = fileEl;
-  this.beginUpload();
-};
-(function() {
-  this.beginUpload = function() {
-    var self = this;
-    // Create invisible frame
-    var iframeId = 'shinyupload_iframe_' + this.id;
-    this.iframe = document.createElement('iframe');
-    this.iframe.id = iframeId;
-    this.iframe.name = iframeId;
-    this.iframe.setAttribute('style', 'position: fixed; top: 0; left: 0; width: 0; height: 0; border: none');
-    $('body').append(this.iframe);
-    var iframeDestroy = function() {
-      // Forces Shiny to flushReact, flush outputs, etc. Without this we get
-      // invalidated reactives, but observers don't actually execute.
-      self.shinyapp.makeRequest('uploadieFinish', [], function(){}, function(){});
-      $(self.iframe).remove();
+      this.form = document.createElement('form');
+      this.form.method = 'POST';
+      this.form.setAttribute('enctype', 'multipart/form-data');
+      this.form.action = "session/" + encodeURI(this.shinyapp.config.sessionId) + "/uploadie/" + encodeURI(this.id);
+      this.form.id = 'shinyupload_form_' + this.id;
+      this.form.target = iframeId;
+      $(this.form).insertAfter(this.fileEl).append(this.fileEl);
+      this.form.submit();
     };
-    if (this.iframe.attachEvent) {
-      this.iframe.attachEvent('onload', iframeDestroy);
-    } else {
-      this.iframe.onload = iframeDestroy;
-    }
+  }).call(IE8FileUploader.prototype);
 
-    this.form = document.createElement('form');
-    this.form.method = 'POST';
-    this.form.setAttribute('enctype', 'multipart/form-data');
-    this.form.action = "session/" + encodeURI(this.shinyapp.config.sessionId) +
-                       "/uploadie/" + encodeURI(this.id);
-    this.form.id = 'shinyupload_form_' + this.id;
-    this.form.target = iframeId;
-    $(this.form).insertAfter(this.fileEl).append(this.fileEl);
-    this.form.submit();
+  var FileUploader = function FileUploader(shinyapp, id, files) {
+    this.shinyapp = shinyapp;
+    this.id = id;
+    FileProcessor.call(this, files);
   };
-}).call(IE8FileUploader.prototype);
-
-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;
-
-    // Reset progress bar
-    this.$setError(null);
-    this.$setActive(true);
-    this.$setVisible(true);
-    this.onProgress(null, 0);
-
-    this.totalBytes = 0;
-    this.progressBytes = 0;
-    $.each(files, function(i, file) {
-      self.totalBytes += file.size;
-    });
+  $.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;
+
+      // Reset progress bar
+      this.$setError(null);
+      this.$setActive(true);
+      this.$setVisible(true);
+      this.onProgress(null, 0);
+
+      this.totalBytes = 0;
+      this.progressBytes = 0;
+      $.each(files, function (i, file) {
+        self.totalBytes += file.size;
+      });
 
-    var fileInfo = $.map(files, function(file, i) {
-      return {
-        name: file.name,
-        size: file.size,
-        type: file.type
-      };
-    });
+      var fileInfo = $.map(files, function (file, i) {
+        return {
+          name: file.name,
+          size: file.size,
+          type: file.type
+        };
+      });
 
-    this.makeRequest(
-      'uploadInit', [fileInfo],
-      function(response) {
+      this.makeRequest('uploadInit', [fileInfo], function (response) {
         self.jobId = response.jobId;
         self.uploadUrl = response.uploadUrl;
         cont();
-      },
-      function(error) {
+      }, function (error) {
         self.onError(error);
       });
-  };
-  this.onFile = function(file, cont) {
-    var self = this;
-    this.onProgress(file, 0);
-
-    $.ajax(this.uploadUrl, {
-      type: 'POST',
-      cache: false,
-      xhr: function() {
-        var xhrVal = $.ajaxSettings.xhr();
-        if (xhrVal.upload) {
-          xhrVal.upload.onprogress = function(e) {
-            if (e.lengthComputable) {
-              self.onProgress(
-                file,
-                (self.progressBytes + e.loaded) / self.totalBytes);
-            }
-          };
+    };
+    this.onFile = function (file, cont) {
+      var self = this;
+      this.onProgress(file, 0);
+
+      $.ajax(this.uploadUrl, {
+        type: 'POST',
+        cache: false,
+        xhr: function xhr() {
+          var xhrVal = $.ajaxSettings.xhr();
+          if (xhrVal.upload) {
+            xhrVal.upload.onprogress = function (e) {
+              if (e.lengthComputable) {
+                self.onProgress(file, (self.progressBytes + e.loaded) / self.totalBytes);
+              }
+            };
+          }
+          return xhrVal;
+        },
+        data: file,
+        processData: false,
+        success: function success() {
+          self.progressBytes += file.size;
+          cont();
+        },
+        error: function error(jqXHR, textStatus, errorThrown) {
+          self.onError(jqXHR.responseText || textStatus);
         }
-        return xhrVal;
-      },
-      data: file,
-      processData: false,
-      success: function() {
-        self.progressBytes += file.size;
-        cont();
-      },
-      error: function(jqXHR, textStatus, errorThrown) {
-        self.onError(jqXHR.responseText || textStatus);
-      }
-    });
-  };
-  this.onComplete = function() {
-    var self = this;
-    this.makeRequest(
-      'uploadEnd', [this.jobId, this.id],
-      function(response) {
+      });
+    };
+    this.onComplete = function () {
+      var self = this;
+      this.makeRequest('uploadEnd', [this.jobId, this.id], function (response) {
         self.$setActive(false);
         self.onProgress(null, 1);
         self.$bar().text('Upload complete');
-      },
-      function(error) {
+      }, function (error) {
         self.onError(error);
       });
-    this.$bar().text('Finishing upload');
-  };
-  this.onError = function(message) {
-    this.$setError(message || '');
-    this.$setActive(false);
-  };
-  this.onAbort = function() {
-    this.$setVisible(false);
-  };
-  this.onProgress = function(file, completed) {
-    this.$bar().width(Math.round(completed*100) + '%');
-    this.$bar().text(file ? file.name : '');
-  };
-  this.$container = function() {
-    return $('#' + $escape(this.id) + '_progress.shiny-file-input-progress');
-  };
-  this.$bar = function() {
-    return $('#' + $escape(this.id) + '_progress.shiny-file-input-progress .progress-bar');
-  };
-  this.$setVisible = function(visible) {
-    this.$container().css('visibility', visible ? 'visible' : 'hidden');
-  };
-  this.$setError = function(error) {
-    this.$bar().toggleClass('bar-danger', (error !== null));
-    if (error !== null) {
-      this.onProgress(null, 1);
-      this.$bar().text(error);
+      this.$bar().text('Finishing upload');
+    };
+    this.onError = function (message) {
+      this.$setError(message || '');
+      this.$setActive(false);
+    };
+    this.onAbort = function () {
+      this.$setVisible(false);
+    };
+    this.onProgress = function (file, completed) {
+      this.$bar().width(Math.round(completed * 100) + '%');
+      this.$bar().text(file ? file.name : '');
+    };
+    this.$container = function () {
+      return $('#' + $escape(this.id) + '_progress.shiny-file-input-progress');
+    };
+    this.$bar = function () {
+      return $('#' + $escape(this.id) + '_progress.shiny-file-input-progress .progress-bar');
+    };
+    this.$setVisible = function (visible) {
+      this.$container().css('visibility', visible ? 'visible' : 'hidden');
+    };
+    this.$setError = function (error) {
+      this.$bar().toggleClass('bar-danger', error !== null);
+      if (error !== null) {
+        this.onProgress(null, 1);
+        this.$bar().text(error);
+      }
+    };
+    this.$setActive = function (active) {
+      this.$container().toggleClass('active', !!active);
+    };
+  }).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;
+    // IE8 here does not necessarily mean literally IE8; it indicates if the web
+    // browser supports the FileList object (IE8/9 do not support it)
+    var IE8 = typeof files === 'undefined';
+    var id = fileInputBinding.getId(evt.target);
+
+    if (!IE8 && files.length === 0) return;
+
+    // Clear data-restore attribute if present.
+    $el.removeAttr('data-restore');
+
+    // Set the label in the text box
+    var $fileText = $el.closest('div.input-group').find('input[type=text]');
+    if (IE8) {
+      // If we're using IE8/9, just use this placeholder
+      $fileText.val("[Uploaded file]");
+    } else if (files.length === 1) {
+      $fileText.val(files[0].name);
+    } else {
+      $fileText.val(files.length + " files");
+    }
+
+    // Start the new upload and put the uploader in 'currentUploader'.
+    if (IE8) {
+      /*jshint nonew:false */
+      new IE8FileUploader(exports.shinyapp, id, evt.target);
+    } else {
+      $el.data('currentUploader', new FileUploader(exports.shinyapp, id, files));
     }
-  };
-  this.$setActive = function(active) {
-    this.$container().toggleClass('active', !!active);
-  };
-}).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;
-  // IE8 here does not necessarily mean literally IE8; it indicates if the web
-  // browser supports the FileList object (IE8/9 do not support it)
-  var IE8 = typeof(files) === 'undefined';
-  var id = fileInputBinding.getId(evt.target);
-
-  if (!IE8 && files.length === 0)
-    return;
-
-  // Start the new upload and put the uploader in 'currentUploader'.
-  if (IE8) {
-    /*jshint nonew:false */
-    new IE8FileUploader(exports.shinyapp, id, evt.target);
-  } else {
-    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');
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/init_shiny.js
+  var fileInputBinding = new InputBinding();
+  $.extend(fileInputBinding, {
+    find: function find(scope) {
+      return $(scope).find('input[type="file"]');
+    },
+    getId: function getId(el) {
+      return InputBinding.prototype.getId.call(this, el) || el.name;
+    },
+    getValue: function getValue(el) {
+      // This returns a non-undefined value only when there's a 'data-restore'
+      // attribute, which is set only when restoring Shiny state. If a file is
+      // uploaded through the browser, 'data-restore' gets cleared.
+      var data = $(el).attr('data-restore');
+      if (data) {
+        data = JSON.parse(data);
+
+        // Set the label in the text box
+        var $fileText = $(el).closest('div.input-group').find('input[type=text]');
+        if (data.name.length === 1) {
+          $fileText.val(data.name[0]);
+        } else {
+          $fileText.val(data.name.length + " files");
+        }
+
+        // Manually set up progress bar. A bit inelegant because it duplicates
+        // code from FileUploader, but duplication is less bad than alternatives.
+        var $progress = $(el).closest('div.form-group').find('.progress');
+        var $bar = $progress.find('.progress-bar');
+        $progress.removeClass('active');
+        $bar.width('100%');
+        $bar.css('visibility', 'visible');
+
+        return data;
+      } else {
+        return null;
+      }
+    },
+    setValue: function setValue(el, value) {
+      // Not implemented
+    },
+    getType: function getType(el) {
+      // This will be used only when restoring a file from a saved state.
+      return 'shiny.file';
+    },
+    subscribe: function subscribe(el, callback) {
+      $(el).on('change.fileInputBinding', uploadFiles);
+    },
+    unsubscribe: function unsubscribe(el) {
+      $(el).off('.fileInputBinding');
+    }
+  });
+  inputBindings.register(fileInputBinding, 'shiny.fileInputBinding');
 
-function initShiny() {
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/init_shiny.js
 
-  var shinyapp = exports.shinyapp = new ShinyApp();
+  function initShiny() {
 
-  function bindOutputs(scope) {
+    var shinyapp = exports.shinyapp = new ShinyApp();
 
-    if (scope === undefined)
-      scope = document;
+    function bindOutputs() {
+      var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
 
-    scope = $(scope);
+      scope = $(scope);
 
-    var bindings = outputBindings.getBindings();
+      var bindings = outputBindings.getBindings();
 
-    for (var i = 0; i < bindings.length; i++) {
-      var binding = bindings[i].binding;
-      var matches = binding.find(scope) || [];
-      for (var j = 0; j < matches.length; j++) {
-        var el = matches[j];
-        var id = binding.getId(el);
+      for (var i = 0; i < bindings.length; i++) {
+        var binding = bindings[i].binding;
+        var matches = binding.find(scope) || [];
+        for (var j = 0; j < matches.length; j++) {
+          var el = matches[j];
+          var id = binding.getId(el);
+
+          // Check if ID is falsy
+          if (!id) continue;
 
-        // Check if ID is falsy
-        if (!id)
-          continue;
+          // In some uncommon cases, elements that are later in the
+          // matches array can be removed from the document by earlier
+          // iterations. See https://github.com/rstudio/shiny/issues/1399
+          if (!$.contains(document, el)) continue;
+
+          var $el = $(el);
+          if ($el.hasClass('shiny-bound-output')) {
+            // Already bound; can happen with nested uiOutput (bindAll
+            // gets called on two ancestors)
+            continue;
+          }
 
-        var $el = $(el);
-        if ($el.hasClass('shiny-bound-output')) {
-          // Already bound; can happen with nested uiOutput (bindAll
-          // gets called on two ancestors)
-          continue;
+          var bindingAdapter = new OutputBindingAdapter(el, binding);
+          shinyapp.bindOutput(id, bindingAdapter);
+          $el.data('shiny-output-binding', bindingAdapter);
+          $el.addClass('shiny-bound-output');
+          $el.trigger({
+            type: 'shiny:bound',
+            binding: binding,
+            bindingType: 'output'
+          });
         }
+      }
+
+      // Send later in case DOM layout isn't final yet.
+      setTimeout(sendImageSize, 0);
+      setTimeout(sendOutputHiddenState, 0);
+    }
+
+    function unbindOutputs() {
+      var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
+      var includeSelf = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+      var outputs = $(scope).find('.shiny-bound-output');
+
+      if (includeSelf && $(scope).hasClass('shiny-bound-output')) {
+        outputs.push(scope);
+      }
 
-        var bindingAdapter = new OutputBindingAdapter(el, binding);
-        shinyapp.bindOutput(id, bindingAdapter);
-        $el.data('shiny-output-binding', bindingAdapter);
-        $el.addClass('shiny-bound-output');
+      for (var i = 0; i < outputs.length; i++) {
+        var $el = $(outputs[i]);
+        var bindingAdapter = $el.data('shiny-output-binding');
+        if (!bindingAdapter) continue;
+        var id = bindingAdapter.binding.getId(outputs[i]);
+        shinyapp.unbindOutput(id, bindingAdapter);
+        $el.removeClass('shiny-bound-output');
+        $el.removeData('shiny-output-binding');
         $el.trigger({
-          type: 'shiny:bound',
-          binding: binding,
+          type: 'shiny:unbound',
+          binding: bindingAdapter.binding,
           bindingType: 'output'
         });
       }
-    }
-
-    // Send later in case DOM layout isn't final yet.
-    setTimeout(sendImageSize, 0);
-    setTimeout(sendOutputHiddenState, 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 $el = $(outputs[i]);
-      var bindingAdapter = $el.data('shiny-output-binding');
-      if (!bindingAdapter)
-        continue;
-      var id = bindingAdapter.binding.getId(outputs[i]);
-      shinyapp.unbindOutput(id, bindingAdapter);
-      $el.removeClass('shiny-bound-output');
-      $el.removeData('shiny-output-binding');
-      $el.trigger({
-        type: 'shiny:unbound',
-        binding: bindingAdapter.binding,
-        bindingType: 'output'
+      // Send later in case DOM layout isn't final yet.
+      setTimeout(sendImageSize, 0);
+      setTimeout(sendOutputHiddenState, 0);
+    }
+
+    var inputBatchSender = new InputBatchSender(shinyapp);
+    var inputsNoResend = new InputNoResendDecorator(inputBatchSender);
+    var inputsEvent = new InputEventDecorator(inputsNoResend);
+    var inputsRate = new InputRateDecorator(inputsEvent);
+    var inputsDefer = new InputDeferDecorator(inputsEvent);
+
+    // By default, use rate decorator
+    var inputs = inputsRate;
+    $('input[type="submit"], button[type="submit"]').each(function () {
+      // If there is a submit button on the page, use defer decorator
+      inputs = inputsDefer;
+      $(this).click(function (event) {
+        event.preventDefault();
+        inputsDefer.submit();
       });
-    }
-
-    setTimeout(sendOutputHiddenState, 0);
-  }
-
-  var inputBatchSender = new InputBatchSender(shinyapp);
-  var inputsNoResend = new InputNoResendDecorator(inputBatchSender);
-  var inputsEvent = new InputEventDecorator(inputsNoResend);
-  var inputsRate = new InputRateDecorator(inputsEvent);
-  var inputsDefer = new InputDeferDecorator(inputsEvent);
-
-  // By default, use rate decorator
-  var inputs = inputsRate;
-  $('input[type="submit"], button[type="submit"]').each(function() {
-    // If there is a submit button on the page, use defer decorator
-    inputs = inputsDefer;
-    $(this).click(function(event) {
-      event.preventDefault();
-      inputsDefer.submit();
     });
-  });
 
-  exports.onInputChange = function(name, value) {
-    inputs.setInput(name, value);
-  };
+    exports.onInputChange = function (name, value) {
+      inputs.setInput(name, value);
+    };
 
-  var boundInputs = {};
+    var boundInputs = {};
 
-  function valueChangeCallback(binding, el, allowDeferred) {
-    var id = binding.getId(el);
-    if (id) {
-      var value = binding.getValue(el);
-      var type = binding.getType(el);
-      if (type)
-        id = id + ":" + type;
-      inputs.setInput(id, value, !allowDeferred);
+    function valueChangeCallback(binding, el, allowDeferred) {
+      var id = binding.getId(el);
+      if (id) {
+        var value = binding.getValue(el);
+        var type = binding.getType(el);
+        if (type) id = id + ":" + type;
+        inputs.setInput(id, value, !allowDeferred);
+      }
     }
-  }
 
-  function bindInputs(scope) {
+    function bindInputs() {
+      var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
 
-    if (scope === undefined)
-      scope = document;
+      var bindings = inputBindings.getBindings();
 
-    var bindings = inputBindings.getBindings();
+      var currentValues = {};
 
-    var currentValues = {};
+      for (var i = 0; i < bindings.length; i++) {
+        var binding = bindings[i].binding;
+        var matches = binding.find(scope) || [];
+        for (var j = 0; j < matches.length; j++) {
+          var el = matches[j];
+          var id = binding.getId(el);
 
-    for (var i = 0; i < bindings.length; i++) {
-      var binding = bindings[i].binding;
-      var matches = binding.find(scope) || [];
-      for (var j = 0; j < matches.length; j++) {
-        var el = matches[j];
-        var id = binding.getId(el);
+          // Check if ID is falsy, or if already bound
+          if (!id || boundInputs[id]) continue;
 
-        // Check if ID is falsy, or if already bound
-        if (!id || boundInputs[id])
-          continue;
+          var type = binding.getType(el);
+          var effectiveId = type ? id + ":" + type : id;
+          currentValues[effectiveId] = binding.getValue(el);
 
-        var type = binding.getType(el);
-        var effectiveId = type ? id + ":" + type : id;
-        currentValues[effectiveId] = binding.getValue(el);
-
-        /*jshint loopfunc:true*/
-        var thisCallback = (function() {
-          var thisBinding = binding;
-          var thisEl = el;
-          return function(allowDeferred) {
-            valueChangeCallback(thisBinding, thisEl, allowDeferred);
+          /*jshint loopfunc:true*/
+          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(el);
+          if (ratePolicy !== null) {
+            inputsRate.setRatePolicy(effectiveId, ratePolicy.policy, ratePolicy.delay);
+          }
+
+          boundInputs[id] = {
+            binding: binding,
+            node: el
           };
-        })();
-
-        binding.subscribe(el, thisCallback);
-        $(el).data('shiny-input-binding', binding);
-        $(el).addClass('shiny-bound-input');
-        var ratePolicy = binding.getRatePolicy(el);
-        if (ratePolicy !== null) {
-          inputsRate.setRatePolicy(
-            effectiveId,
-            ratePolicy.policy,
-            ratePolicy.delay);
+
+          $(el).trigger({
+            type: 'shiny:bound',
+            binding: binding,
+            bindingType: 'input'
+          });
+
+          if (shinyapp.isConnected()) {
+            valueChangeCallback(binding, el, false);
+          }
         }
+      }
 
-        boundInputs[id] = {
-          binding: binding,
-          node: el
-        };
+      return currentValues;
+    }
+
+    function unbindInputs() {
+      var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
+      var includeSelf = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+      var inputs = $(scope).find('.shiny-bound-input');
+
+      if (includeSelf && $(scope).hasClass('shiny-bound-input')) {
+        inputs.push(scope);
+      }
 
+      for (var i = 0; i < inputs.length; i++) {
+        var el = inputs[i];
+        var binding = $(el).data('shiny-input-binding');
+        if (!binding) continue;
+        var id = binding.getId(el);
+        $(el).removeClass('shiny-bound-input');
+        delete boundInputs[id];
+        binding.unsubscribe(el);
         $(el).trigger({
-          type: 'shiny:bound',
+          type: 'shiny:unbound',
           binding: binding,
           bindingType: 'input'
         });
-
-        if (shinyapp.isConnected()) {
-          valueChangeCallback(binding, el, false);
-        }
       }
     }
 
-    return currentValues;
-  }
-
-  function unbindInputs(scope) {
-    if (scope === undefined)
-      scope = document;
-
-    var inputs = $(scope).find('.shiny-bound-input');
-    for (var i = 0; i < inputs.length; i++) {
-      var el = inputs[i];
-      var binding = $(el).data('shiny-input-binding');
-      if (!binding)
-        continue;
-      var id = binding.getId(el);
-      $(el).removeClass('shiny-bound-input');
-      delete boundInputs[id];
-      binding.unsubscribe(el);
-      $(el).trigger({
-        type: 'shiny:unbound',
-        binding: binding,
-        bindingType: 'input'
-      });
+    function _bindAll(scope) {
+      bindOutputs(scope);
+      return bindInputs(scope);
     }
-  }
+    function unbindAll(scope) {
+      var includeSelf = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
 
-  function _bindAll(scope) {
-    bindOutputs(scope);
-    return 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);
-    });
+      unbindInputs(scope, includeSelf);
+      unbindOutputs(scope, includeSelf);
+    }
+    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);
+      });
 
-    // Not sure if the iframe stuff is an intrinsic part of bindAll, but bindAll
-    // is a convenient place to hang it. bindAll will be called anytime new HTML
-    // appears that might contain inputs/outputs; it's reasonable to assume that
-    // any such HTML may contain iframes as well.
-    initDeferredIframes();
-  };
-  exports.unbindAll = unbindAll;
+      // Not sure if the iframe stuff is an intrinsic part of bindAll, but bindAll
+      // is a convenient place to hang it. bindAll will be called anytime new HTML
+      // appears that might contain inputs/outputs; it's reasonable to assume that
+      // any such HTML may contain iframes as well.
+      initDeferredIframes();
+    };
+    exports.unbindAll = unbindAll;
 
-  // Calls .initialize() for all of the input objects in all input bindings,
-  // in the given scope.
-  function initializeInputs(scope) {
-    if (scope === undefined)
-      scope = document;
+    // Calls .initialize() for all of the input objects in all input bindings,
+    // in the given scope.
+    function initializeInputs() {
+      var scope = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
 
-    var bindings = inputBindings.getBindings();
+      var bindings = inputBindings.getBindings();
 
-    // Iterate over all bindings
-    for (var i = 0; i < bindings.length; i++) {
-      var binding = bindings[i].binding;
-      var inputObjects = binding.find(scope) || [];
+      // Iterate over all bindings
+      for (var i = 0; i < bindings.length; i++) {
+        var binding = bindings[i].binding;
+        var inputObjects = binding.find(scope) || [];
 
-      // Iterate over all input objects for this binding
-      for (var j = 0; j < inputObjects.length; j++) {
-        binding.initialize(inputObjects[j]);
+        // Iterate over all input objects for this binding
+        for (var j = 0; j < inputObjects.length; j++) {
+          if (!inputObjects[j]._shiny_initialized) {
+            inputObjects[j]._shiny_initialized = true;
+            binding.initialize(inputObjects[j]);
+          }
+        }
       }
     }
-  }
-  exports.initializeInputs = initializeInputs;
-
-  function getIdFromEl(el) {
-    var $el = $(el);
-    var bindingAdapter = $el.data("shiny-output-binding");
-    if (!bindingAdapter)
-      return null;
-    else
-      return bindingAdapter.getId();
-  }
+    exports.initializeInputs = initializeInputs;
 
+    function getIdFromEl(el) {
+      var $el = $(el);
+      var bindingAdapter = $el.data("shiny-output-binding");
+      if (!bindingAdapter) return null;else return bindingAdapter.getId();
+    }
 
-  // Initialize all input objects in the document, before binding
-  initializeInputs(document);
-
-  var initialValues = _bindAll(document);
+    // Initialize all input objects in the document, before binding
+    initializeInputs(document);
 
+    var initialValues = _bindAll(document);
 
-  // The server needs to know the size of each image and plot output element,
-  // in case it is auto-sizing
-  $('.shiny-image-output, .shiny-plot-output').each(function() {
-    var id = getIdFromEl(this);
-    if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
-      initialValues['.clientdata_output_' + id + '_width'] = this.offsetWidth;
-      initialValues['.clientdata_output_' + id + '_height'] = this.offsetHeight;
-    }
-  });
-  function doSendImageSize() {
-    $('.shiny-image-output, .shiny-plot-output').each(function() {
+    // The server needs to know the size of each image and plot output element,
+    // in case it is auto-sizing
+    $('.shiny-image-output, .shiny-plot-output').each(function () {
       var id = getIdFromEl(this);
       if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
-        inputs.setInput('.clientdata_output_' + id + '_width', this.offsetWidth);
-        inputs.setInput('.clientdata_output_' + id + '_height', this.offsetHeight);
+        initialValues['.clientdata_output_' + id + '_width'] = this.offsetWidth;
+        initialValues['.clientdata_output_' + id + '_height'] = this.offsetHeight;
       }
     });
-    $('.shiny-bound-output').each(function() {
-      var $this = $(this), binding = $this.data('shiny-output-binding');
-      $this.trigger({
-        type: 'shiny:visualchange',
-        visible: !isHidden(this),
-        binding: binding
+    function doSendImageSize() {
+      $('.shiny-image-output, .shiny-plot-output').each(function () {
+        var id = getIdFromEl(this);
+        if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
+          inputs.setInput('.clientdata_output_' + id + '_width', this.offsetWidth);
+          inputs.setInput('.clientdata_output_' + id + '_height', this.offsetHeight);
+        }
+      });
+      $('.shiny-bound-output').each(function () {
+        var $this = $(this),
+            binding = $this.data('shiny-output-binding');
+        $this.trigger({
+          type: 'shiny:visualchange',
+          visible: !isHidden(this),
+          binding: binding
+        });
+        binding.onResize();
       });
-      binding.onResize();
+    }
+    var sendImageSizeDebouncer = new Debouncer(null, doSendImageSize, 0);
+    function sendImageSize() {
+      sendImageSizeDebouncer.normalCall();
+    }
+    // Make sure sendImageSize actually gets called before the inputBatchSender
+    // sends data to the server.
+    inputBatchSender.lastChanceCallback.push(function () {
+      if (sendImageSizeDebouncer.isPending()) sendImageSizeDebouncer.immediateCall();
     });
-  }
-  var sendImageSizeDebouncer = new Debouncer(null, doSendImageSize, 0);
-  function sendImageSize() {
-    sendImageSizeDebouncer.normalCall();
-  }
-  // Make sure sendImageSize actually gets called before the inputBatchSender
-  // sends data to the server.
-  inputBatchSender.lastChanceCallback.push(function() {
-    if (sendImageSizeDebouncer.isPending())
-      sendImageSizeDebouncer.immediateCall();
-  });
 
-  // Return true if the object or one of its ancestors in the DOM tree has
-  // style='display:none'; otherwise return false.
-  function isHidden(obj) {
-    // null means we've hit the top of the tree. If width or height is
-    // non-zero, then we know that no ancestor has display:none.
-    if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) {
-      return false;
-    } else if (getStyle(obj, 'display') === 'none') {
-      return true;
-    } else {
-      return(isHidden(obj.parentNode));
-    }
-  }
-  var lastKnownVisibleOutputs = {};
-  // Set initial state of outputs to hidden, if needed
-  $('.shiny-bound-output').each(function() {
-    var id = getIdFromEl(this);
-    if (isHidden(this)) {
-      initialValues['.clientdata_output_' + id + '_hidden'] = true;
-    } else {
-      lastKnownVisibleOutputs[id] = true;
-      initialValues['.clientdata_output_' + id + '_hidden'] = false;
+    // Return true if the object or one of its ancestors in the DOM tree has
+    // style='display:none'; otherwise return false.
+    function isHidden(obj) {
+      // null means we've hit the top of the tree. If width or height is
+      // non-zero, then we know that no ancestor has display:none.
+      if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) {
+        return false;
+      } else if (getStyle(obj, 'display') === 'none') {
+        return true;
+      } else {
+        return isHidden(obj.parentNode);
+      }
     }
-  });
-  // Send update when hidden state changes
-  function doSendOutputHiddenState() {
-    var visibleOutputs = {};
-    $('.shiny-bound-output').each(function() {
+    var lastKnownVisibleOutputs = {};
+    // Set initial state of outputs to hidden, if needed
+    $('.shiny-bound-output').each(function () {
       var id = getIdFromEl(this);
-      delete lastKnownVisibleOutputs[id];
-      // Assume that the object is hidden when width and height are 0
-      var hidden = isHidden(this), evt = {
-        type: 'shiny:visualchange',
-        visible: !hidden
-      };
-      if (hidden) {
-        inputs.setInput('.clientdata_output_' + id + '_hidden', true);
+      if (isHidden(this)) {
+        initialValues['.clientdata_output_' + id + '_hidden'] = true;
       } else {
-        visibleOutputs[id] = true;
-        inputs.setInput('.clientdata_output_' + id + '_hidden', false);
+        lastKnownVisibleOutputs[id] = true;
+        initialValues['.clientdata_output_' + id + '_hidden'] = false;
       }
-      var $this = $(this);
-      evt.binding = $this.data('shiny-output-binding');
-      $this.trigger(evt);
     });
-    // Anything left in lastKnownVisibleOutputs is orphaned
-    for (var name in lastKnownVisibleOutputs) {
-      if (lastKnownVisibleOutputs.hasOwnProperty(name))
-        inputs.setInput('.clientdata_output_' + name + '_hidden', true);
-    }
-    // Update the visible outputs for next time
-    lastKnownVisibleOutputs = visibleOutputs;
-  }
-  // sendOutputHiddenState gets called each time DOM elements are shown or
-  // hidden. This can be in the hundreds or thousands of times at startup.
-  // We'll debounce it, so that we do the actual work once per tick.
-  var sendOutputHiddenStateDebouncer = new Debouncer(null, doSendOutputHiddenState, 0);
-  function sendOutputHiddenState() {
-    sendOutputHiddenStateDebouncer.normalCall();
-  }
-  // We need to make sure doSendOutputHiddenState actually gets called before
-  // the inputBatchSender sends data to the server. The lastChanceCallback
-  // here does that - if the debouncer has a pending call, flush it.
-  inputBatchSender.lastChanceCallback.push(function() {
-    if (sendOutputHiddenStateDebouncer.isPending())
-      sendOutputHiddenStateDebouncer.immediateCall();
-  });
+    // Send update when hidden state changes
+    function doSendOutputHiddenState() {
+      var visibleOutputs = {};
+      $('.shiny-bound-output').each(function () {
+        var id = getIdFromEl(this);
+        delete lastKnownVisibleOutputs[id];
+        // Assume that the object is hidden when width and height are 0
+        var hidden = isHidden(this),
+            evt = {
+          type: 'shiny:visualchange',
+          visible: !hidden
+        };
+        if (hidden) {
+          inputs.setInput('.clientdata_output_' + id + '_hidden', true);
+        } else {
+          visibleOutputs[id] = true;
+          inputs.setInput('.clientdata_output_' + id + '_hidden', false);
+        }
+        var $this = $(this);
+        evt.binding = $this.data('shiny-output-binding');
+        $this.trigger(evt);
+      });
+      // Anything left in lastKnownVisibleOutputs is orphaned
+      for (var name in lastKnownVisibleOutputs) {
+        if (lastKnownVisibleOutputs.hasOwnProperty(name)) inputs.setInput('.clientdata_output_' + name + '_hidden', true);
+      }
+      // Update the visible outputs for next time
+      lastKnownVisibleOutputs = visibleOutputs;
+    }
+    // sendOutputHiddenState gets called each time DOM elements are shown or
+    // hidden. This can be in the hundreds or thousands of times at startup.
+    // We'll debounce it, so that we do the actual work once per tick.
+    var sendOutputHiddenStateDebouncer = new Debouncer(null, doSendOutputHiddenState, 0);
+    function sendOutputHiddenState() {
+      sendOutputHiddenStateDebouncer.normalCall();
+    }
+    // We need to make sure doSendOutputHiddenState actually gets called before
+    // the inputBatchSender sends data to the server. The lastChanceCallback
+    // here does that - if the debouncer has a pending call, flush it.
+    inputBatchSender.lastChanceCallback.push(function () {
+      if (sendOutputHiddenStateDebouncer.isPending()) sendOutputHiddenStateDebouncer.immediateCall();
+    });
 
-  // Given a namespace and a handler function, return a function that invokes
-  // the handler only when e's namespace matches. For example, if the
-  // namespace is "bs", it would match when e.namespace is "bs" or "bs.tab".
-  // If the namespace is "bs.tab", it would match for "bs.tab", but not "bs".
-  function filterEventsByNamespace(namespace, handler) {
-    namespace = namespace.split(".");
+    // Given a namespace and a handler function, return a function that invokes
+    // the handler only when e's namespace matches. For example, if the
+    // namespace is "bs", it would match when e.namespace is "bs" or "bs.tab".
+    // If the namespace is "bs.tab", it would match for "bs.tab", but not "bs".
+    function filterEventsByNamespace(namespace, handler) {
+      namespace = namespace.split(".");
 
-    return function(e) {
-      var eventNamespace = e.namespace.split(".");
+      return function (e) {
+        var eventNamespace = e.namespace.split(".");
 
-      // If any of the namespace strings aren't present in this event, quit.
-      for (var i=0; i<namespace.length; i++) {
-        if (eventNamespace.indexOf(namespace[i]) === -1)
-          return;
-      }
+        // If any of the namespace strings aren't present in this event, quit.
+        for (var i = 0; i < namespace.length; i++) {
+          if (eventNamespace.indexOf(namespace[i]) === -1) return;
+        }
 
-      handler.apply(this, arguments);
-    };
-  }
+        handler.apply(this, arguments);
+      };
+    }
 
-  // The size of each image may change either because the browser window was
-  // resized, or because a tab was shown/hidden (hidden elements report size
-  // 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, sendImageSize));
-  // Need to register callbacks for each Bootstrap 3 class.
-  var bs3classes = ['modal', 'dropdown', 'tab', 'tooltip', 'popover', 'collapse'];
-  $.each(bs3classes, function(idx, classname) {
-    $('body').on('shown.bs.' + classname + '.sendImageSize', '*',
-      filterEventsByNamespace('bs', sendImageSize));
-    $('body').on('shown.bs.' + classname + '.sendOutputHiddenState ' +
-                 'hidden.bs.' + classname + '.sendOutputHiddenState',
-                 '*', filterEventsByNamespace('bs', sendOutputHiddenState));
-  });
+    // The size of each image may change either because the browser window was
+    // resized, or because a tab was shown/hidden (hidden elements report size
+    // 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, sendImageSize));
+    // Need to register callbacks for each Bootstrap 3 class.
+    var bs3classes = ['modal', 'dropdown', 'tab', 'tooltip', 'popover', 'collapse'];
+    $.each(bs3classes, function (idx, classname) {
+      $('body').on('shown.bs.' + classname + '.sendImageSize', '*', filterEventsByNamespace('bs', sendImageSize));
+      $('body').on('shown.bs.' + classname + '.sendOutputHiddenState ' + 'hidden.bs.' + classname + '.sendOutputHiddenState', '*', filterEventsByNamespace('bs', sendOutputHiddenState));
+    });
 
-  // This is needed for Bootstrap 2 compatibility and for non-Bootstrap
-  // related shown/hidden events (like conditionalPanel)
-  $('body').on('shown.sendImageSize', '*', sendImageSize);
-  $('body').on('shown.sendOutputHiddenState hidden.sendOutputHiddenState', '*',
-               sendOutputHiddenState);
+    // This is needed for Bootstrap 2 compatibility and for non-Bootstrap
+    // related shown/hidden events (like conditionalPanel)
+    $('body').on('shown.sendImageSize', '*', sendImageSize);
+    $('body').on('shown.sendOutputHiddenState hidden.sendOutputHiddenState', '*', sendOutputHiddenState);
 
-  // Send initial pixel ratio, and update it if it changes
-  initialValues['.clientdata_pixelratio'] = pixelRatio();
-  $(window).resize(function() {
-    inputs.setInput('.clientdata_pixelratio', pixelRatio());
-  });
+    // Send initial pixel ratio, and update it if it changes
+    initialValues['.clientdata_pixelratio'] = pixelRatio();
+    $(window).resize(function () {
+      inputs.setInput('.clientdata_pixelratio', pixelRatio());
+    });
+
+    // Send initial URL
+    initialValues['.clientdata_url_protocol'] = window.location.protocol;
+    initialValues['.clientdata_url_hostname'] = window.location.hostname;
+    initialValues['.clientdata_url_port'] = window.location.port;
+    initialValues['.clientdata_url_pathname'] = window.location.pathname;
+    initialValues['.clientdata_url_search'] = window.location.search;
+    // This is only the initial value of the hash. The hash can change, but
+    // a reactive version of this isn't sent because w atching for changes can
+    // require polling on some browsers. The JQuery hashchange plugin can be
+    // used if this capability is important.
+    initialValues['.clientdata_url_hash_initial'] = window.location.hash;
+
+    // The server needs to know what singletons were rendered as part of
+    // the page loading
+    var singletonText = initialValues['.clientdata_singletons'] = $('script[type="application/shiny-singletons"]').text();
+    singletons.registerNames(singletonText.split(/,/));
+
+    var dependencyText = $('script[type="application/html-dependencies"]').text();
+    $.each(dependencyText.split(/;/), function (i, depStr) {
+      var match = /\s*^(.+)\[(.+)\]\s*$/.exec(depStr);
+      if (match) {
+        registerDependency(match[1], match[2]);
+      }
+    });
 
-  // Send initial URL
-  initialValues['.clientdata_url_protocol'] = window.location.protocol;
-  initialValues['.clientdata_url_hostname'] = window.location.hostname;
-  initialValues['.clientdata_url_port']     = window.location.port;
-  initialValues['.clientdata_url_pathname'] = window.location.pathname;
-  initialValues['.clientdata_url_search']   = window.location.search;
-  // This is only the initial value of the hash. The hash can change, but
-  // a reactive version of this isn't sent because w atching for changes can
-  // require polling on some browsers. The JQuery hashchange plugin can be
-  // used if this capability is important.
-  initialValues['.clientdata_url_hash_initial'] = window.location.hash;
-
-  // The server needs to know what singletons were rendered as part of
-  // the page loading
-  var singletonText = initialValues['.clientdata_singletons'] =
-      $('script[type="application/shiny-singletons"]').text();
-  singletons.registerNames(singletonText.split(/,/));
-
-  var dependencyText = $('script[type="application/html-dependencies"]').text();
-  $.each(dependencyText.split(/;/), function(i, depStr) {
-    var match = /\s*^(.+)\[(.+)\]\s*$/.exec(depStr);
-    if (match) {
-      registerDependency(match[1], match[2]);
+    // We've collected all the initial values--start the server process!
+    inputsNoResend.reset(initialValues);
+    shinyapp.connect(initialValues);
+    $(document).one("shiny:connected", function () {
+      initDeferredIframes();
+    });
+  } // function initShiny()
+
+
+  // Give any deferred iframes a chance to load.
+  function initDeferredIframes() {
+    if (!window.Shiny || !window.Shiny.shinyapp || !window.Shiny.shinyapp.isConnected()) {
+      // If somehow we accidentally call this before the server connection is
+      // established, just ignore the call. At the time of this writing it
+      // doesn't happen, but it's easy to imagine a later refactoring putting
+      // us in this situation and it'd be hard to notice with either manual
+      // testing or automated tests, because the only effect is on HTTP request
+      // timing. (Update: Actually Aron saw this being called without even
+      // window.Shiny being defined, but it was hard to repro.)
+      return;
     }
-  });
 
-  // We've collected all the initial values--start the server process!
-  inputsNoResend.reset(initialValues);
-  shinyapp.connect(initialValues);
-  $(document).one("shiny:connected", function() {
-    initDeferredIframes();
-  });
-} // function initShiny()
-
-
-// Give any deferred iframes a chance to load.
-function initDeferredIframes() {
-  if (!window.Shiny || !window.Shiny.shinyapp || !window.Shiny.shinyapp.isConnected()) {
-    // If somehow we accidentally call this before the server connection is
-    // established, just ignore the call. At the time of this writing it
-    // doesn't happen, but it's easy to imagine a later refactoring putting
-    // us in this situation and it'd be hard to notice with either manual
-    // testing or automated tests, because the only effect is on HTTP request
-    // timing. (Update: Actually Aron saw this being called without even
-    // window.Shiny being defined, but it was hard to repro.)
-    return;
+    $(".shiny-frame-deferred").each(function (i, el) {
+      var $el = $(el);
+      $el.removeClass("shiny-frame-deferred");
+      $el.attr("src", $el.attr("data-deferred-src"));
+      $el.attr("data-deferred-src", null);
+    });
   }
 
-  $(".shiny-frame-deferred").each(function (i, el) {
-    var $el = $(el);
-    $el.removeClass("shiny-frame-deferred");
-    $el.attr("src", $el.attr("data-deferred-src"));
-    $el.attr("data-deferred-src", null);
+  $(function () {
+    // Init Shiny a little later than document ready, so user code can
+    // run first (i.e. to register bindings)
+    setTimeout(initShiny, 1);
   });
-}
-
-$(function() {
-  // Init Shiny a little later than document ready, so user code can
-  // run first (i.e. to register bindings)
-  setTimeout(initShiny, 1);
-});
-
-//---------------------------------------------------------------------
-// Source file: ../srcjs/reactlog.js
 
-$(document).on('keydown', function(e) {
-  if (e.which !== 114 || (!e.ctrlKey && !e.metaKey) || (e.shiftKey || e.altKey))
-    return;
-  var url = 'reactlog?w=' + window.escape(exports.shinyapp.config.workerId) +
-    "&s=" + window.escape(exports.shinyapp.config.sessionId);
-  window.open(url);
-  e.preventDefault();
-});
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/reactlog.js
 
-//---------------------------------------------------------------------
-// Source file: ../srcjs/_end.js
+  $(document).on('keydown', function (e) {
+    if (e.which !== 114 || !e.ctrlKey && !e.metaKey || e.shiftKey || e.altKey) return;
+    var url = 'reactlog?w=' + window.escape(exports.shinyapp.config.workerId) + "&s=" + window.escape(exports.shinyapp.config.sessionId);
+    window.open(url);
+    e.preventDefault();
+  });
 
+  //---------------------------------------------------------------------
+  // Source file: ../srcjs/_end.js
 })();
 
-//# sourceMappingURL=shiny.js.map
\ No newline at end of file
+//# sourceMappingURL=shiny.js.map
+//# sourceMappingURL=shiny.js.map
diff --git a/inst/www/shared/shiny.js.map b/inst/www/shared/shiny.js.map
index fdab9e4..7fdad4c 100644
--- a/inst/www/shared/shiny.js.map
+++ b/inst/www/shared/shiny.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../../../srcjs/_start.js","../../../srcjs/utils.js","../../../srcjs/browser.js","../../../srcjs/input_rate.js","../../../srcjs/shinyapp.js","../../../srcjs/file_processor.js","../../../srcjs/binding_registry.js","../../../srcjs/output_binding.js","../../../srcjs/output_binding_text.js","../../../srcjs/output_binding_image.js","../../../srcjs/output_binding_html.js","../../../srcjs/output_binding_downloadlink.js","../../../srcjs/output_binding_datatable.js","../.. [...]
\ No newline at end of file
+{"version":3,"sources":["../../srcjs/_start.js","../../srcjs/utils.js","../../srcjs/browser.js","../../srcjs/input_rate.js","../../srcjs/shinyapp.js","../../srcjs/notifications.js","../../srcjs/modal.js","../../srcjs/file_processor.js","../../srcjs/binding_registry.js","../../srcjs/output_binding.js","../../srcjs/output_binding_text.js","../../srcjs/output_binding_image.js","../../srcjs/output_binding_html.js","../../srcjs/output_binding_downloadlink.js","../../srcjs/output_binding_datat [...]
\ No newline at end of file
diff --git a/inst/www/shared/shiny.min.js b/inst/www/shared/shiny.min.js
index ec2f46e..ec1a6df 100644
--- a/inst/www/shared/shiny.min.js
+++ b/inst/www/shared/shiny.min.js
@@ -1,6 +1,6 @@
-/*! shiny 0.13.2 | (c) 2012-2016 RStudio, Inc. | License: GPL-3 | file LICENSE */
+/*! shiny 0.14.2 | (c) 2012-2016 RStudio, Inc. | License: GPL-3 | file LICENSE */
 
-!function(){function escapeHTML(a){return a.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"&#x2F;")}function randomId(){return Math.floor(4294967296+64424509440*Math.random()).toString(16)}function strToBool(a){if(!a||!a.toLowerCase)return void 0;switch(a.toLowerCase()){case"true":return!0;case"false":return!1;default:return void 0}}function getStyle(a,b){var c;if(a.currentStyle)c=a.currentStyle[b];else if(wind [...]
-d.direction=c.brushDirection,d.brushId=a,d.outputId=e,exports.onInputChange(a,d),b.data("mostRecentBrush",!0),imageOutputBinding.find(document).trigger("shiny-internal:brushed",d)}function h(a){if(!(r.isBrushing()||r.isDragging()||r.isResizing())&&1===a.which){var b=d.mouseOffset(a);if(!c.brushClip||d.isInPanel(b,q))if(r.up({x:NaN,y:NaN}),r.down(b),r.isInResizeArea(b))r.startResizing(b),$(document).on("mousemove.image_brush",l).on("mouseup.image_brush",o);else if(r.isInsideBrush(b))r.sta [...]
-return $("#"+$escape(this.id)+"_progress.shiny-file-input-progress")},this.$bar=function(){return $("#"+$escape(this.id)+"_progress.shiny-file-input-progress .progress-bar")},this.$setVisible=function(a){this.$container().css("visibility",a?"visible":"hidden")},this.$setError=function(a){this.$bar().toggleClass("bar-danger",null!==a),null!==a&&(this.onProgress(null,1),this.$bar().text(a))},this.$setActive=function(a){this.$container().toggleClass("active",!!a)}}.call(FileUploader.prototy [...]
+"use strict";var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a};!function(){function escapeHTML(a){return a.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"&#x2F;")}function randomId(){return Math.floor(4294967296+64424509440*Math.random()).toString(16 [...]
+},this.onValueError=function(a,b){this.renderError(a,b)},this.renderError=function(a,b){if(this.clearError(a),""===b.message)return void $(a).empty();var c="shiny-output-error";null!==b.type&&(c=c+" "+$.map(asArray(b.type),function(a){return c+"-"+a}).join(" ")),$(a).addClass(c).text(b.message)},this.clearError=function(a){$(a).attr("class",function(a,b){return b.replace(/(^|\s)shiny-output-error\S*/g,"")})},this.showProgress=function(a,b){var c="recalculating";b?$(a).addClass(c):$(a).re [...]
+var c=this._selectize(a);"undefined"!=typeof c?c.setValue(b):$(a).val(b)},getState:function(a){for(var b=new Array(a.length),c=0;c<a.length;c++)b[c]={value:a[c].value,label:a[c].label};return{label:$(a).parent().find('label[for="'+$escape(a.id)+'"]').text(),value:this.getValue(a),options:b}},receiveMessage:function(a,b){var c,d=$(a);if(b.hasOwnProperty("options")&&(c=this._selectize(a),c&&c.destroy(),d.empty().append(b.options),this._selectize(a)),b.hasOwnProperty("config")&&(d.parent(). [...]
 //# sourceMappingURL=shiny.min.js.map
\ No newline at end of file
diff --git a/inst/www/shared/shiny.min.js.map b/inst/www/shared/shiny.min.js.map
index 01b817c..a496671 100644
--- a/inst/www/shared/shiny.min.js.map
+++ b/inst/www/shared/shiny.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"shiny.min.js","sources":["../../../srcjs/_start.js","../../../srcjs/utils.js","../../../srcjs/browser.js","../../../srcjs/input_rate.js","../../../srcjs/shinyapp.js","../../../srcjs/file_processor.js","../../../srcjs/binding_registry.js","../../../srcjs/output_binding.js","../../../srcjs/output_binding_text.js","../../../srcjs/output_binding_image.js","../../../srcjs/output_binding_html.js","../../../srcjs/output_binding_downloadlink.js","../../../srcjs/output_bindin [...]
\ No newline at end of file
+{"version":3,"sources":["../../srcjs/_start.js","../../srcjs/utils.js","../../srcjs/input_rate.js","../../srcjs/output_binding_html.js","../../srcjs/input_binding_slider.js","../../srcjs/input_binding_fileinput.js","../../srcjs/init_shiny.js","../../srcjs/browser.js","../../srcjs/shinyapp.js","../../srcjs/notifications.js","../../srcjs/modal.js","../../srcjs/file_processor.js","../../srcjs/binding_registry.js","../../srcjs/output_binding.js","../../srcjs/output_binding_text.js","../../sr [...]
\ No newline at end of file
diff --git a/man/Progress.Rd b/man/Progress.Rd
index 0090760..2c71cc5 100644
--- a/man/Progress.Rd
+++ b/man/Progress.Rd
@@ -27,6 +27,11 @@ de-emphasized appearance relative to \code{message}.}
 the progress bar, relative to \code{min} and \code{max}.
 \code{NULL} hides the progress bar, if it is currently visible.}
 
+\item{style}{Progress display style. If \code{"notification"} (the default),
+the progress indicator will show using Shiny's notification API. If
+\code{"old"}, use the same HTML and CSS used in Shiny 0.13.2 and below
+(this is for backward-compatibility).}
+
 \item{amount}{Single-element numeric vector; the value at which to set
 the progress bar, relative to \code{min} and \code{max}.
 \code{NULL} hides the progress bar, if it is currently visible.}
@@ -48,6 +53,14 @@ created, and it will be displayed the first time the \code{set}
 method is called. Calling \code{close} will cause the progress panel
 to be removed.
 
+As of version 0.14, the progress indicators use Shiny's new notification API.
+If you want to use the old styling (for example, you may have used customized
+CSS), you can use \code{style="old"} each time you call
+\code{Progress$new()}. If you don't want to set the style each time
+\code{Progress$new} is called, you can instead call
+\code{\link{shinyOptions}(progress.style="old")} just once, inside the server
+function.
+
 \strong{Methods}
   \describe{
     \item{\code{initialize(session, min = 0, max = 1)}}{
@@ -69,11 +82,16 @@ to be removed.
   }
 }
 \examples{
-\dontrun{
-# server.R
-shinyServer(function(input, output, session) {
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  plotOutput("plot")
+)
+
+server <- function(input, output, session) {
   output$plot <- renderPlot({
-    progress <- shiny::Progress$new(session, min=1, max=15)
+    progress <- Progress$new(session, min=1, max=15)
     on.exit(progress$close())
 
     progress$set(message = 'Calculation in progress',
@@ -85,7 +103,9 @@ shinyServer(function(input, output, session) {
     }
     plot(cars)
   })
-})
+}
+
+shinyApp(ui, server)
 }
 }
 \seealso{
diff --git a/man/actionButton.Rd b/man/actionButton.Rd
index 9353a0f..98ddd51 100644
--- a/man/actionButton.Rd
+++ b/man/actionButton.Rd
@@ -27,19 +27,29 @@ Creates an action button or link whose value is initially zero, and increments b
 each time it is pressed.
 }
 \examples{
-\dontrun{
-# In server.R
-output$distPlot <- renderPlot({
-  # Take a dependency on input$goButton
-  input$goButton
-
-  # Use isolate() to avoid dependency on input$obs
-  dist <- isolate(rnorm(input$obs))
-  hist(dist)
-})
-
-# In ui.R
-actionButton("goButton", "Go!")
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  sliderInput("obs", "Number of observations", 0, 1000, 500),
+  actionButton("goButton", "Go!"),
+  plotOutput("distPlot")
+)
+
+server <- function(input, output) {
+  output$distPlot <- renderPlot({
+    # Take a dependency on input$goButton. This will run once initially,
+    # because the value changes from NULL to 0.
+    input$goButton
+
+    # Use isolate() to avoid dependency on input$obs
+    dist <- isolate(rnorm(input$obs))
+    hist(dist)
+  })
+}
+
+shinyApp(ui, server)
+
 }
 
 }
@@ -52,6 +62,6 @@ Other input.elements: \code{\link{checkboxGroupInput}},
   \code{\link{numericInput}}, \code{\link{passwordInput}},
   \code{\link{radioButtons}}, \code{\link{selectInput}},
   \code{\link{sliderInput}}, \code{\link{submitButton}},
-  \code{\link{textInput}}
+  \code{\link{textAreaInput}}, \code{\link{textInput}}
 }
 
diff --git a/man/addResourcePath.Rd b/man/addResourcePath.Rd
index ca9b42b..b99a8e2 100644
--- a/man/addResourcePath.Rd
+++ b/man/addResourcePath.Rd
@@ -28,7 +28,6 @@ You can call \code{addResourcePath} multiple times for a given
 }
 \examples{
 addResourcePath('datasets', system.file('data', package='datasets'))
-
 }
 \seealso{
 \code{\link{singleton}}
diff --git a/man/applyInputHandlers.Rd b/man/applyInputHandlers.Rd
new file mode 100644
index 0000000..4f2e130
--- /dev/null
+++ b/man/applyInputHandlers.Rd
@@ -0,0 +1,30 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/server-input-handlers.R
+\name{applyInputHandlers}
+\alias{applyInputHandlers}
+\title{Apply input handlers to raw input values}
+\usage{
+applyInputHandlers(inputs, shinysession = getDefaultReactiveDomain())
+}
+\arguments{
+\item{inputs}{A named list of input values.}
+
+\item{shinysession}{A Shiny session object.}
+}
+\description{
+The purpose of this function is to make it possible for external packages to
+test Shiny inputs. It takes a named list of raw input values, applies input
+handlers to those values, and then returns a named list of the processed
+values.
+}
+\details{
+The raw input values should be in a named list. Some values may have names
+like \code{"x:shiny.date"}. This function would apply the \code{"shiny.date"}
+input handler to the value, and then rename the result to \code{"x"}, in the
+output.
+}
+\seealso{
+registerInputHandler
+}
+\keyword{internal}
+
diff --git a/man/bookmarkButton.Rd b/man/bookmarkButton.Rd
new file mode 100644
index 0000000..183e3f5
--- /dev/null
+++ b/man/bookmarkButton.Rd
@@ -0,0 +1,73 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/bookmark-state.R
+\name{bookmarkButton}
+\alias{bookmarkButton}
+\title{Create a button for bookmarking/sharing}
+\usage{
+bookmarkButton(label = "Bookmark...", icon = shiny::icon("link", lib =
+  "glyphicon"),
+  title = "Bookmark this application's state and get a URL for sharing.", ...,
+  id = "._bookmark_")
+}
+\arguments{
+\item{label}{The contents of the button or link--usually a text label, but
+you could also use any other HTML, like an image.}
+
+\item{icon}{An optional \code{\link{icon}} to appear on the button.}
+
+\item{title}{A tooltip that is shown when the mouse cursor hovers over the
+button.}
+
+\item{...}{Named attributes to be applied to the button or link.}
+
+\item{id}{An ID for the bookmark button. The only time it is necessary to set
+the ID unless you have more than one bookmark button in your application.
+If you specify an input ID, it should be excluded from bookmarking with
+\code{\link{setBookmarkExclude}}, and you must create an observer that
+does the bookmarking when the button is pressed. See the examples below.}
+}
+\description{
+A \code{bookmarkButton} is a \code{\link{actionButton}} with a default label
+that consists of a link icon and the text "Bookmark...". It is meant to be
+used for bookmarking state.
+}
+\examples{
+## Only run these examples in interactive sessions
+if (interactive()) {
+
+# This example shows how to use multiple bookmark buttons. If you only need
+# a single bookmark button, see examples in ?enableBookmarking.
+ui <- function(request) {
+  fluidPage(
+    tabsetPanel(id = "tabs",
+      tabPanel("One",
+        checkboxInput("chk1", "Checkbox 1"),
+        bookmarkButton(id = "bookmark1")
+      ),
+      tabPanel("Two",
+        checkboxInput("chk2", "Checkbox 2"),
+        bookmarkButton(id = "bookmark2")
+      )
+    )
+  )
+}
+server <- function(input, output, session) {
+  # Need to exclude the buttons from themselves being bookmarked
+  setBookmarkExclude(c("bookmark1", "bookmark2"))
+
+  # Trigger bookmarking with either button
+  observeEvent(input$bookmark1, {
+    session$doBookmark()
+  })
+  observeEvent(input$bookmark2, {
+    session$doBookmark()
+  })
+}
+enableBookmarking(store = "url")
+shinyApp(ui, server)
+}
+}
+\seealso{
+\code{\link{enableBookmarking}} for more examples.
+}
+
diff --git a/man/checkboxGroupInput.Rd b/man/checkboxGroupInput.Rd
index 55f5d98..1f407e0 100644
--- a/man/checkboxGroupInput.Rd
+++ b/man/checkboxGroupInput.Rd
@@ -31,11 +31,25 @@ 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"))
+## Only run examples in interactive R sessions
+if (interactive()) {
 
+ui <- fluidPage(
+  checkboxGroupInput("variable", "Variables to show:",
+                     c("Cylinders" = "cyl",
+                       "Transmission" = "am",
+                       "Gears" = "gear")),
+  tableOutput("data")
+)
+
+server <- function(input, output) {
+  output$data <- renderTable({
+    mtcars[, c("mpg", input$variable), drop = FALSE]
+  }, rownames = TRUE)
+}
+
+shinyApp(ui, server)
+}
 }
 \seealso{
 \code{\link{checkboxInput}}, \code{\link{updateCheckboxGroupInput}}
@@ -46,6 +60,6 @@ Other input.elements: \code{\link{actionButton}},
   \code{\link{numericInput}}, \code{\link{passwordInput}},
   \code{\link{radioButtons}}, \code{\link{selectInput}},
   \code{\link{sliderInput}}, \code{\link{submitButton}},
-  \code{\link{textInput}}
+  \code{\link{textAreaInput}}, \code{\link{textInput}}
 }
 
diff --git a/man/checkboxInput.Rd b/man/checkboxInput.Rd
index d9fb5e1..6668f4a 100644
--- a/man/checkboxInput.Rd
+++ b/man/checkboxInput.Rd
@@ -23,7 +23,18 @@ A checkbox control that can be added to a UI definition.
 Create a checkbox that can be used to specify logical values.
 }
 \examples{
-checkboxInput("outliers", "Show outliers", FALSE)
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  checkboxInput("somevalue", "Some value", FALSE),
+  verbatimTextOutput("value")
+)
+server <- function(input, output) {
+  output$value <- renderText({ input$somevalue })
+}
+shinyApp(ui, server)
+}
 }
 \seealso{
 \code{\link{checkboxGroupInput}}, \code{\link{updateCheckboxInput}}
@@ -34,6 +45,7 @@ Other input.elements: \code{\link{actionButton}},
   \code{\link{fileInput}}, \code{\link{numericInput}},
   \code{\link{passwordInput}}, \code{\link{radioButtons}},
   \code{\link{selectInput}}, \code{\link{sliderInput}},
-  \code{\link{submitButton}}, \code{\link{textInput}}
+  \code{\link{submitButton}}, \code{\link{textAreaInput}},
+  \code{\link{textInput}}
 }
 
diff --git a/man/column.Rd b/man/column.Rd
index 8dc45a5..a244f9f 100644
--- a/man/column.Rd
+++ b/man/column.Rd
@@ -23,24 +23,43 @@ Create a column for use within a  \code{\link{fluidRow}} or
 \code{\link{fixedRow}}
 }
 \examples{
-fluidRow(
-  column(4,
-    sliderInput("obs", "Number of observations:",
-                min = 1, max = 1000, value = 500)
-  ),
-  column(8,
-    plotOutput("distPlot")
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  fluidRow(
+    column(4,
+      sliderInput("obs", "Number of observations:",
+                  min = 1, max = 1000, value = 500)
+    ),
+    column(8,
+      plotOutput("distPlot")
+    )
   )
 )
 
-fluidRow(
-  column(width = 4,
-    "4"
-  ),
-  column(width = 3, offset = 2,
-    "3 offset 2"
+server <- function(input, output) {
+  output$distPlot <- renderPlot({
+    hist(rnorm(input$obs))
+  })
+}
+
+shinyApp(ui, server)
+
+
+
+ui <- fluidPage(
+  fluidRow(
+    column(width = 4,
+      "4"
+    ),
+    column(width = 3, offset = 2,
+      "3 offset 2"
+    )
   )
 )
+shinyApp(ui, server = function(input, output) { })
+}
 }
 \seealso{
 \code{\link{fluidRow}}, \code{\link{fixedRow}}.
diff --git a/man/conditionalPanel.Rd b/man/conditionalPanel.Rd
index d0fd794..7841a03 100644
--- a/man/conditionalPanel.Rd
+++ b/man/conditionalPanel.Rd
@@ -55,6 +55,5 @@ sidebarPanel(
       )
    )
 )
-
 }
 
diff --git a/man/createWebDependency.Rd b/man/createWebDependency.Rd
index 3905eb7..baa302b 100644
--- a/man/createWebDependency.Rd
+++ b/man/createWebDependency.Rd
@@ -8,7 +8,7 @@ createWebDependency(dependency)
 }
 \arguments{
 \item{dependency}{A single HTML dependency object, created using
-\code{\link{htmlDependency}}. If the \code{src} value is named, then
+\code{\link[htmltools]{htmlDependency}}. If the \code{src} value is named, then
 \code{href} and/or \code{file} names must be present.}
 }
 \value{
diff --git a/man/dateInput.Rd b/man/dateInput.Rd
index ebc8ed8..fef0fc0 100644
--- a/man/dateInput.Rd
+++ b/man/dateInput.Rd
@@ -14,8 +14,8 @@ dateInput(inputId, label, value = NULL, min = NULL, max = NULL,
 \item{label}{Display label for the control, or \code{NULL} for no label.}
 
 \item{value}{The starting date. Either a Date object, or a string in
-\code{yyyy-mm-dd} format. If NULL (the default), will use the current
-date in the client's time zone.}
+\code{yyyy-mm-dd} format. If NULL (the default), will use the current date
+in the client's time zone.}
 
 \item{min}{The minimum allowed date. Either a Date object, or a string in
 \code{yyyy-mm-dd} format.}
@@ -26,17 +26,20 @@ date in the client's time zone.}
 \item{format}{The format of the date to display in the browser. Defaults to
 \code{"yyyy-mm-dd"}.}
 
-\item{startview}{The date range shown when the input object is first
-clicked. Can be "month" (the default), "year", or "decade".}
+\item{startview}{The date range shown when the input object is first clicked.
+Can be "month" (the default), "year", or "decade".}
 
 \item{weekstart}{Which day is the start of the week. Should be an integer
 from 0 (Sunday) to 6 (Saturday).}
 
 \item{language}{The language used for month and day names. Default is "en".
-Other valid values include "bg", "ca", "cs", "da", "de", "el", "es", "fi",
-"fr", "he", "hr", "hu", "id", "is", "it", "ja", "kr", "lt", "lv", "ms",
-"nb", "nl", "pl", "pt", "pt-BR", "ro", "rs", "rs-latin", "ru", "sk", "sl",
-"sv", "sw", "th", "tr", "uk", "zh-CN", and "zh-TW".}
+Other valid values include "ar", "az", "bg", "bs", "ca", "cs", "cy", "da",
+"de", "el", "en-AU", "en-GB", "eo", "es", "et", "eu", "fa", "fi", "fo",
+"fr-CH", "fr", "gl", "he", "hr", "hu", "hy", "id", "is", "it-CH", "it",
+"ja", "ka", "kh", "kk", "ko", "kr", "lt", "lv", "me", "mk", "mn", "ms",
+"nb", "nl-BE", "nl", "no", "pl", "pt-BR", "pt", "ro", "rs-latin", "rs",
+"ru", "sk", "sl", "sq", "sr-latin", "sr", "sv", "sw", "th", "tr", "uk",
+"vi", "zh-CN", and "zh-TW".}
 
 \item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
 see \code{\link{validateCssUnit}}.}
@@ -63,26 +66,33 @@ the browser. It allows the following values:
 }
 }
 \examples{
-dateInput("date", "Date:", value = "2012-02-29")
+## Only run examples in interactive R sessions
+if (interactive()) {
 
-# Default value is the date in client's time zone
-dateInput("date", "Date:")
+ui <- fluidPage(
+  dateInput("date1", "Date:", value = "2012-02-29"),
 
-# value is always yyyy-mm-dd, even if the display format is different
-dateInput("date", "Date:", value = "2012-02-29", format = "mm/dd/yy")
+  # Default value is the date in client's time zone
+  dateInput("date2", "Date:"),
 
-# Pass in a Date object
-dateInput("date", "Date:", value = Sys.Date()-10)
+  # value is always yyyy-mm-dd, even if the display format is different
+  dateInput("date3", "Date:", value = "2012-02-29", format = "mm/dd/yy"),
 
-# Use different language and different first day of week
-dateInput("date", "Date:",
-          language = "de",
-          weekstart = 1)
+  # Pass in a Date object
+  dateInput("date4", "Date:", value = Sys.Date()-10),
 
-# Start with decade view instead of default month view
-dateInput("date", "Date:",
-          startview = "decade")
+  # Use different language and different first day of week
+  dateInput("date5", "Date:",
+          language = "ru",
+          weekstart = 1),
 
+  # Start with decade view instead of default month view
+  dateInput("date6", "Date:",
+            startview = "decade")
+)
+
+shinyApp(ui, server = function(input, output) { })
+}
 }
 \seealso{
 \code{\link{dateRangeInput}}, \code{\link{updateDateInput}}
@@ -94,6 +104,6 @@ Other input.elements: \code{\link{actionButton}},
   \code{\link{numericInput}}, \code{\link{passwordInput}},
   \code{\link{radioButtons}}, \code{\link{selectInput}},
   \code{\link{sliderInput}}, \code{\link{submitButton}},
-  \code{\link{textInput}}
+  \code{\link{textAreaInput}}, \code{\link{textInput}}
 }
 
diff --git a/man/dateRangeInput.Rd b/man/dateRangeInput.Rd
index 1fd7356..0c72c12 100644
--- a/man/dateRangeInput.Rd
+++ b/man/dateRangeInput.Rd
@@ -30,17 +30,20 @@ date in the client's time zone.}
 \item{format}{The format of the date to display in the browser. Defaults to
 \code{"yyyy-mm-dd"}.}
 
-\item{startview}{The date range shown when the input object is first
-clicked. Can be "month" (the default), "year", or "decade".}
+\item{startview}{The date range shown when the input object is first clicked.
+Can be "month" (the default), "year", or "decade".}
 
 \item{weekstart}{Which day is the start of the week. Should be an integer
 from 0 (Sunday) to 6 (Saturday).}
 
 \item{language}{The language used for month and day names. Default is "en".
-Other valid values include "bg", "ca", "cs", "da", "de", "el", "es", "fi",
-"fr", "he", "hr", "hu", "id", "is", "it", "ja", "kr", "lt", "lv", "ms",
-"nb", "nl", "pl", "pt", "pt-BR", "ro", "rs", "rs-latin", "ru", "sk", "sl",
-"sv", "sw", "th", "tr", "uk", "zh-CN", and "zh-TW".}
+Other valid values include "ar", "az", "bg", "bs", "ca", "cs", "cy", "da",
+"de", "el", "en-AU", "en-GB", "eo", "es", "et", "eu", "fa", "fi", "fo",
+"fr-CH", "fr", "gl", "he", "hr", "hu", "hy", "id", "is", "it-CH", "it",
+"ja", "ka", "kh", "kk", "ko", "kr", "lt", "lv", "me", "mk", "mn", "ms",
+"nb", "nl-BE", "nl", "no", "pl", "pt-BR", "pt", "ro", "rs-latin", "rs",
+"ru", "sk", "sl", "sq", "sr-latin", "sr", "sv", "sw", "th", "tr", "uk",
+"vi", "zh-CN", and "zh-TW".}
 
 \item{separator}{String to display between the start and end input boxes.}
 
@@ -69,37 +72,44 @@ the browser. It allows the following values:
 }
 }
 \examples{
-dateRangeInput("daterange", "Date range:",
-               start = "2001-01-01",
-               end   = "2010-12-31")
-
-# Default start and end is the current date in the client's time zone
-dateRangeInput("daterange", "Date range:")
-
-# start and end are always specified in yyyy-mm-dd, even if the display
-# format is different
-dateRangeInput("daterange", "Date range:",
-               start  = "2001-01-01",
-               end    = "2010-12-31",
-               min    = "2001-01-01",
-               max    = "2012-12-21",
-               format = "mm/dd/yy",
-               separator = " - ")
-
-# Pass in Date objects
-dateRangeInput("daterange", "Date range:",
-               start = Sys.Date()-10,
-               end = Sys.Date()+10)
-
-# Use different language and different first day of week
-dateRangeInput("daterange", "Date range:",
-               language = "de",
-               weekstart = 1)
-
-# Start with decade view instead of default month view
-dateRangeInput("daterange", "Date range:",
-               startview = "decade")
-
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  dateRangeInput("daterange1", "Date range:",
+                 start = "2001-01-01",
+                 end   = "2010-12-31"),
+
+  # Default start and end is the current date in the client's time zone
+  dateRangeInput("daterange2", "Date range:"),
+
+  # start and end are always specified in yyyy-mm-dd, even if the display
+  # format is different
+  dateRangeInput("daterange3", "Date range:",
+                 start  = "2001-01-01",
+                 end    = "2010-12-31",
+                 min    = "2001-01-01",
+                 max    = "2012-12-21",
+                 format = "mm/dd/yy",
+                 separator = " - "),
+
+  # Pass in Date objects
+  dateRangeInput("daterange4", "Date range:",
+                 start = Sys.Date()-10,
+                 end = Sys.Date()+10),
+
+  # Use different language and different first day of week
+  dateRangeInput("daterange5", "Date range:",
+                 language = "de",
+                 weekstart = 1),
+
+  # Start with decade view instead of default month view
+  dateRangeInput("daterange6", "Date range:",
+                 startview = "decade")
+)
+
+shinyApp(ui, server = function(input, output) { })
+}
 }
 \seealso{
 \code{\link{dateInput}}, \code{\link{updateDateRangeInput}}
@@ -110,6 +120,7 @@ Other input.elements: \code{\link{actionButton}},
   \code{\link{fileInput}}, \code{\link{numericInput}},
   \code{\link{passwordInput}}, \code{\link{radioButtons}},
   \code{\link{selectInput}}, \code{\link{sliderInput}},
-  \code{\link{submitButton}}, \code{\link{textInput}}
+  \code{\link{submitButton}}, \code{\link{textAreaInput}},
+  \code{\link{textInput}}
 }
 
diff --git a/man/downloadHandler.Rd b/man/downloadHandler.Rd
index 60965b4..6decb7a 100644
--- a/man/downloadHandler.Rd
+++ b/man/downloadHandler.Rd
@@ -4,7 +4,7 @@
 \alias{downloadHandler}
 \title{File Downloads}
 \usage{
-downloadHandler(filename, content, contentType = NA)
+downloadHandler(filename, content, contentType = NA, outputArgs = list())
 }
 \arguments{
 \item{filename}{A string of the filename, including extension, that the
@@ -22,6 +22,10 @@ function.)}
 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.}
+
+\item{outputArgs}{A list of arguments to be passed through to the implicit
+call to \code{\link{downloadButton}} when \code{downloadHandler} is used
+in an interactive R Markdown document.}
 }
 \description{
 Allows content from the Shiny application to be made available to the user as
@@ -33,20 +37,28 @@ the user initiates the download. Assign the return value to a slot on
 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)
-  }
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  downloadLink("downloadData", "Download")
 )
 
-# In ui.R:
-downloadLink('downloadData', 'Download')
+server <- function(input, output) {
+  # Our dataset
+  data <- mtcars
+
+  output$downloadData <- downloadHandler(
+    filename = function() {
+      paste("data-", Sys.Date(), ".csv", sep="")
+    },
+    content = function(file) {
+      write.csv(data, file)
+    }
+  )
 }
 
+shinyApp(ui, server)
+}
 }
 
diff --git a/man/enableBookmarking.Rd b/man/enableBookmarking.Rd
new file mode 100644
index 0000000..24e8105
--- /dev/null
+++ b/man/enableBookmarking.Rd
@@ -0,0 +1,231 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/bookmark-state.R
+\name{enableBookmarking}
+\alias{enableBookmarking}
+\title{Enable bookmarking for a Shiny application}
+\usage{
+enableBookmarking(store = c("url", "server", "disable"))
+}
+\arguments{
+\item{store}{Either \code{"url"}, which encodes all of the relevant values in
+a URL, \code{"server"}, which saves to disk on the server, or
+\code{"disable"}, which disables any previously-enabled bookmarking.}
+}
+\description{
+There are two types of bookmarking: saving an application's state to disk on
+the server, and encoding the application's state in a URL. For state that has
+been saved to disk, the state can be restored with the corresponding state
+ID. For URL-encoded state, the state of the application is encoded in the
+URL, and no server-side storage is needed.
+
+URL-encoded bookmarking is appropriate for applications where there not many
+input values that need to be recorded. Some browsers have a length limit for
+URLs of about 2000 characters, and if there are many inputs, the length of
+the URL can exceed that limit.
+
+Saved-on-server bookmarking is appropriate when there are many inputs, or
+when the bookmarked state requires storing files.
+}
+\details{
+For restoring state to work properly, the UI must be a function that takes
+one argument, \code{request}. In most Shiny applications, the UI is not a
+function; it might have the form \code{fluidPage(....)}. Converting it to a
+function is as simple as wrapping it in a function, as in
+\code{function(request) \{ fluidPage(....) \}}.
+
+By default, all input values will be bookmarked, except for the values of
+passwordInputs. fileInputs will be saved if the state is saved on a server,
+but not if the state is encoded in a URL.
+
+When bookmarking state, arbitrary values can be stored, by passing a function
+as the \code{onBookmark} argument. That function will be passed a
+\code{ShinySaveState} object. The \code{values} field of the object is a list
+which can be manipulated to save extra information. Additionally, if the
+state is being saved on the server, and the \code{dir} field of that object
+can be used to save extra information to files in that directory.
+
+For saved-to-server state, this is how the state directory is chosen:
+\itemize{
+  \item If running in a hosting environment such as Shiny Server or
+    Connect, the hosting environment will choose the directory.
+  \item If running an app in a directory with \code{\link{runApp}()}, the
+    saved states will be saved in a subdirectory of the app called
+   shiny_bookmarks.
+  \item If running a Shiny app object that is generated from code (not run
+    from a directory), the saved states will be saved in a subdirectory of
+    the current working directory called shiny_bookmarks.
+}
+
+When used with \code{\link{shinyApp}()}, this function must be called before
+\code{shinyApp()}, or in the \code{shinyApp()}'s \code{onStart} function. An
+alternative to calling the \code{enableBookmarking()} function is to use the
+\code{enableBookmarking} \emph{argument} for \code{shinyApp()}. See examples
+below.
+}
+\examples{
+## Only run these examples in interactive R sessions
+if (interactive()) {
+
+# Basic example with state encoded in URL
+ui <- function(request) {
+  fluidPage(
+    textInput("txt", "Text"),
+    checkboxInput("chk", "Checkbox"),
+    bookmarkButton()
+  )
+}
+server <- function(input, output, session) { }
+enableBookmarking("url")
+shinyApp(ui, server)
+
+
+# An alternative to calling enableBookmarking(): use shinyApp's
+# enableBookmarking argument
+shinyApp(ui, server, enableBookmarking = "url")
+
+
+# Same basic example with state saved to disk
+enableBookmarking("server")
+shinyApp(ui, server)
+
+
+# Save/restore arbitrary values
+ui <- function(req) {
+  fluidPage(
+    textInput("txt", "Text"),
+    checkboxInput("chk", "Checkbox"),
+    bookmarkButton(),
+    br(),
+    textOutput("lastSaved")
+  )
+}
+server <- function(input, output, session) {
+  vals <- reactiveValues(savedTime = NULL)
+  output$lastSaved <- renderText({
+    if (!is.null(vals$savedTime))
+      paste("Last saved at", vals$savedTime)
+    else
+      ""
+  })
+
+  onBookmark(function(state) {
+    vals$savedTime <- Sys.time()
+    # state is a mutable reference object, and we can add arbitrary values
+    # to it.
+    state$values$time <- vals$savedTime
+  })
+  onRestore(function(state) {
+    vals$savedTime <- state$values$time
+  })
+}
+enableBookmarking(store = "url")
+shinyApp(ui, server)
+
+
+# Usable with dynamic UI (set the slider, then change the text input,
+# click the bookmark button)
+ui <- function(request) {
+  fluidPage(
+    sliderInput("slider", "Slider", 1, 100, 50),
+    uiOutput("ui"),
+    bookmarkButton()
+  )
+}
+server <- function(input, output, session) {
+  output$ui <- renderUI({
+    textInput("txt", "Text", input$slider)
+  })
+}
+enableBookmarking("url")
+shinyApp(ui, server)
+
+
+# Exclude specific inputs (The only input that will be saved in this
+# example is chk)
+ui <- function(request) {
+  fluidPage(
+    passwordInput("pw", "Password"), # Passwords are never saved
+    sliderInput("slider", "Slider", 1, 100, 50), # Manually excluded below
+    checkboxInput("chk", "Checkbox"),
+    bookmarkButton()
+  )
+}
+server <- function(input, output, session) {
+  setBookmarkExclude("slider")
+}
+enableBookmarking("url")
+shinyApp(ui, server)
+
+
+# Update the browser's location bar every time an input changes. This should
+# not be used with enableBookmarking("server"), because that would create a
+# new saved state on disk every time the user changes an input.
+ui <- function(req) {
+  fluidPage(
+    textInput("txt", "Text"),
+    checkboxInput("chk", "Checkbox")
+  )
+}
+server <- function(input, output, session) {
+  observe({
+    # Trigger this observer every time an input changes
+    reactiveValuesToList(input)
+    session$doBookmark()
+  })
+  onBookmarked(function(url) {
+    updateQueryString(url)
+  })
+}
+enableBookmarking("url")
+shinyApp(ui, server)
+
+
+# Save/restore uploaded files
+ui <- function(request) {
+  fluidPage(
+    sidebarLayout(
+      sidebarPanel(
+        fileInput("file1", "Choose CSV File", multiple = TRUE,
+          accept = c(
+            "text/csv",
+            "text/comma-separated-values,text/plain",
+            ".csv"
+          )
+        ),
+        tags$hr(),
+        checkboxInput("header", "Header", TRUE),
+        bookmarkButton()
+      ),
+      mainPanel(
+        tableOutput("contents")
+      )
+    )
+  )
+}
+server <- function(input, output) {
+  output$contents <- renderTable({
+    inFile <- input$file1
+    if (is.null(inFile))
+      return(NULL)
+
+    if (nrow(inFile) == 1) {
+      read.csv(inFile$datapath, header = input$header)
+    } else {
+      data.frame(x = "multiple files")
+    }
+  })
+}
+enableBookmarking("server")
+shinyApp(ui, server)
+
+}
+}
+\seealso{
+\code{\link{onBookmark}}, \code{\link{onBookmarked}},
+  \code{\link{onRestore}}, and \code{\link{onRestored}} for registering
+  callback functions that are invoked when the state is bookmarked or
+  restored.
+
+  Also see \code{\link{updateQueryString}}.
+}
+
diff --git a/man/exportTestValues.Rd b/man/exportTestValues.Rd
new file mode 100644
index 0000000..bcfbbc8
--- /dev/null
+++ b/man/exportTestValues.Rd
@@ -0,0 +1,70 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/test-export.R
+\name{exportTestValues}
+\alias{exportTestValues}
+\title{Register expressions for export in test mode}
+\usage{
+exportTestValues(..., quoted_ = FALSE, env_ = parent.frame(),
+  session_ = getDefaultReactiveDomain())
+}
+\arguments{
+\item{...}{Named arguments that are quoted or unquoted expressions that will
+be captured and evaluated when API endpoint is visited.}
+
+\item{quoted_}{Are the expression quoted? Default is \code{FALSE}.}
+
+\item{env_}{The environment in which the expression should be evaluated.}
+
+\item{session_}{A Shiny session object.}
+}
+\description{
+This function registers expressions that will be evaluated when a test export
+event occurs. These events are triggered by accessing an API endpoint URL.
+}
+\details{
+This function only has an effect if the global option \code{shiny.testmode}
+is set to \code{TRUE}.
+}
+\examples{
+## Only run this example in interactive R sessions
+if (interactive()) {
+
+options(shiny.testmode = TRUE)
+
+# This application shows the test endpoint URL; clicking on it will
+# fetch the input, output, and exported values in JSON format.
+shinyApp(
+  ui = basicPage(
+    h4("Snapshot URL: "),
+    uiOutput("url"),
+    h4("Current values:"),
+    verbatimTextOutput("values"),
+    actionButton("inc", "Increment x")
+  ),
+
+  server = function(input, output, session) {
+    vals <- reactiveValues(x = 1)
+    y <- reactive({ vals$x + 1 })
+
+    observeEvent(input$inc, {
+      vals$x <<- vals$x + 1
+    })
+
+    exportTestValues(
+      x = vals$x,
+      y = y()
+    )
+
+    output$url <- renderUI({
+      url <- session$getTestEndpointUrl(format="json")
+      a(href = url, url)
+    })
+
+    output$values <- renderText({
+      paste0("vals$x: ", vals$x, "\\ny: ", y())
+    })
+  }
+)
+}
+}
+
diff --git a/man/exprToFunction.Rd b/man/exprToFunction.Rd
index 8dac3f4..827261c 100644
--- a/man/exprToFunction.Rd
+++ b/man/exprToFunction.Rd
@@ -57,6 +57,5 @@ tripleA <- renderTriple({
 
 isolate(tripleA())
 # "text, text, text"
-
 }
 
diff --git a/man/fileInput.Rd b/man/fileInput.Rd
index 103f856..9ac884b 100644
--- a/man/fileInput.Rd
+++ b/man/fileInput.Rd
@@ -42,6 +42,47 @@ the following columns:
   operation.}
 }
 }
+\examples{
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  sidebarLayout(
+    sidebarPanel(
+      fileInput("file1", "Choose CSV File",
+        accept = c(
+          "text/csv",
+          "text/comma-separated-values,text/plain",
+          ".csv")
+        ),
+      tags$hr(),
+      checkboxInput("header", "Header", TRUE)
+    ),
+    mainPanel(
+      tableOutput("contents")
+    )
+  )
+)
+
+server <- function(input, output) {
+  output$contents <- renderTable({
+    # input$file1 will be NULL initially. After the user selects
+    # and uploads a file, it will be a data frame with 'name',
+    # 'size', 'type', and 'datapath' columns. The 'datapath'
+    # column will contain the local filenames where the data can
+    # be found.
+    inFile <- input$file1
+
+    if (is.null(inFile))
+      return(NULL)
+
+    read.csv(inFile$datapath, header = input$header)
+  })
+}
+
+shinyApp(ui, server)
+}
+}
 \seealso{
 Other input.elements: \code{\link{actionButton}},
   \code{\link{checkboxGroupInput}},
@@ -49,6 +90,7 @@ Other input.elements: \code{\link{actionButton}},
   \code{\link{dateRangeInput}}, \code{\link{numericInput}},
   \code{\link{passwordInput}}, \code{\link{radioButtons}},
   \code{\link{selectInput}}, \code{\link{sliderInput}},
-  \code{\link{submitButton}}, \code{\link{textInput}}
+  \code{\link{submitButton}}, \code{\link{textAreaInput}},
+  \code{\link{textInput}}
 }
 
diff --git a/man/fillPage.Rd b/man/fillPage.Rd
index 3daf4e4..6d5dcc8 100644
--- a/man/fillPage.Rd
+++ b/man/fillPage.Rd
@@ -80,6 +80,5 @@ fillPage(
     div(style = "background-color: blue; width: 100\%; height: 100\%;")
   )
 )
-
 }
 
diff --git a/man/fillRow.Rd b/man/fillRow.Rd
index ce05dd5..9f03e5a 100644
--- a/man/fillRow.Rd
+++ b/man/fillRow.Rd
@@ -54,10 +54,7 @@ If you try to use \code{fillRow} and \code{fillCol} inside of other
   }
 }
 \examples{
-\donttest{
 # Only run this example in interactive R sessions.
-# NOTE: This example should be run with example(fillRow, ask = FALSE) to
-# avoid being prompted to hit Enter during plot rendering.
 if (interactive()) {
 
 ui <- fillPage(fillRow(
@@ -78,5 +75,4 @@ shinyApp(ui, server)
 
 }
 }
-}
 
diff --git a/man/fixedPage.Rd b/man/fixedPage.Rd
index 41f2acd..0e1b084 100644
--- a/man/fixedPage.Rd
+++ b/man/fixedPage.Rd
@@ -46,7 +46,10 @@ See the \href{http://shiny.rstudio.com/articles/layout-guide.html}{
   pages.
 }
 \examples{
-shinyUI(fixedPage(
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fixedPage(
   title = "Hello, Shiny!",
   fixedRow(
     column(width = 4,
@@ -56,7 +59,10 @@ shinyUI(fixedPage(
       "3 offset 2"
     )
   )
-))
+)
+
+shinyApp(ui, server = function(input, output) { })
+}
 
 }
 \seealso{
diff --git a/man/flowLayout.Rd b/man/flowLayout.Rd
index 7283395..f6ab508 100644
--- a/man/flowLayout.Rd
+++ b/man/flowLayout.Rd
@@ -20,11 +20,16 @@ well with elements that have a percentage-based width (e.g.
 \code{\link{plotOutput}} at its default setting of \code{width = "100\%"}).
 }
 \examples{
-flowLayout(
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- flowLayout(
   numericInput("rows", "How many rows?", 5),
   selectInput("letter", "Which letter?", LETTERS),
   sliderInput("value", "What value?", 0, 100, 50)
 )
+shinyApp(ui, server = function(input, output) { })
+}
 }
 \seealso{
 \code{\link{verticalLayout}}
diff --git a/man/fluidPage.Rd b/man/fluidPage.Rd
index 21ec781..5f5b1ca 100644
--- a/man/fluidPage.Rd
+++ b/man/fluidPage.Rd
@@ -45,7 +45,11 @@ See the \href{http://shiny.rstudio.com/articles/layout-guide.html}{
   pages.
 }
 \examples{
-shinyUI(fluidPage(
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+# Example of UI with fluidPage
+ui <- fluidPage(
 
   # Application title
   titlePanel("Hello Shiny!"),
@@ -66,9 +70,21 @@ shinyUI(fluidPage(
       plotOutput("distPlot")
     )
   )
-))
+)
+
+# Server logic
+server <- function(input, output) {
+  output$distPlot <- renderPlot({
+    hist(rnorm(input$obs))
+  })
+}
 
-shinyUI(fluidPage(
+# Complete app with UI and server components
+shinyApp(ui, server)
+
+
+# UI demonstrating column layouts
+ui <- fluidPage(
   title = "Hello Shiny!",
   fluidRow(
     column(width = 4,
@@ -78,8 +94,10 @@ shinyUI(fluidPage(
       "3 offset 2"
     )
   )
-))
+)
 
+shinyApp(ui, server = function(input, output) { })
+}
 }
 \seealso{
 \code{\link{column}}, \code{\link{sidebarLayout}}
diff --git a/man/freezeReactiveValue.Rd b/man/freezeReactiveValue.Rd
new file mode 100644
index 0000000..444965f
--- /dev/null
+++ b/man/freezeReactiveValue.Rd
@@ -0,0 +1,61 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/reactives.R
+\name{freezeReactiveValue}
+\alias{freezeReactiveValue}
+\title{Freeze a reactive value}
+\usage{
+freezeReactiveValue(x, name)
+}
+\arguments{
+\item{x}{A \code{\link{reactiveValues}} object (like \code{input}).}
+
+\item{name}{The name of a value in the \code{\link{reactiveValues}} object.}
+}
+\description{
+This freezes a reactive value. If the value is accessed while frozen, a
+"silent" exception is raised and the operation is stopped. This is the same
+thing that happens if \code{req(FALSE)} is called. The value is thawed
+(un-frozen; accessing it will no longer raise an exception) when the current
+reactive domain is flushed. In a Shiny application, this occurs after all of
+the observers are executed.
+}
+\examples{
+## Only run this examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  selectInput("data", "Data Set", c("mtcars", "pressure")),
+  checkboxGroupInput("cols", "Columns (select 2)", character(0)),
+  plotOutput("plot")
+)
+
+server <- function(input, output, session) {
+  observe({
+    data <- get(input$data)
+    # Sets a flag on input$cols to essentially do req(FALSE) if input$cols
+    # is accessed. Without this, an error will momentarily show whenever a
+    # new data set is selected.
+    freezeReactiveValue(input, "cols")
+    updateCheckboxGroupInput(session, "cols", choices = names(data))
+  })
+
+  output$plot <- renderPlot({
+    # When a new data set is selected, input$cols will have been invalidated
+    # above, and this will essentially do the same as req(FALSE), causing
+    # this observer to stop and raise a silent exception.
+    cols <- input$cols
+    data <- get(input$data)
+
+    if (length(cols) == 2) {
+      plot(data[[ cols[1] ]], data[[ cols[2] ]])
+    }
+  })
+}
+
+shinyApp(ui, server)
+}
+}
+\seealso{
+\code{\link{req}}
+}
+
diff --git a/man/icon.Rd b/man/icon.Rd
index 8a0353d..2b19bb8 100644
--- a/man/icon.Rd
+++ b/man/icon.Rd
@@ -36,12 +36,11 @@ icon("cog", lib = "glyphicon") # From glyphicon library
 # add an icon to a submit button
 submitButton("Update View", icon = icon("refresh"))
 
-shinyUI(navbarPage("App Title",
+navbarPage("App Title",
   tabPanel("Plot", icon = icon("bar-chart-o")),
   tabPanel("Summary", icon = icon("list-alt")),
   tabPanel("Table", icon = icon("table"))
-))
-
+)
 }
 \seealso{
 For lists of available icons, see
diff --git a/man/insertUI.Rd b/man/insertUI.Rd
new file mode 100644
index 0000000..b6c70c9
--- /dev/null
+++ b/man/insertUI.Rd
@@ -0,0 +1,89 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/insert-ui.R
+\name{insertUI}
+\alias{insertUI}
+\title{Insert UI objects}
+\usage{
+insertUI(selector, where = c("beforeBegin", "afterBegin", "beforeEnd",
+  "afterEnd"), ui, multiple = FALSE, immediate = FALSE,
+  session = getDefaultReactiveDomain())
+}
+\arguments{
+\item{selector}{A string that is accepted by jQuery's selector (i.e. the
+string \code{s} to be placed in a \code{$(s)} jQuery call). This selector
+will determine the element(s) relative to which you want to insert your
+UI object.}
+
+\item{where}{Where your UI object should go relative to the selector:
+\describe{
+  \item{\code{beforeBegin}}{Before the selector element itself}
+  \item{\code{afterBegin}}{Just inside the selector element, before its
+  first child}
+  \item{\code{beforeEnd}}{Just inside the selector element, after its
+  last child (default)}
+  \item{\code{afterEnd}}{After the selector element itself}
+}
+Adapted from
+\href{https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML}{here}.}
+
+\item{ui}{The UI object you want to insert. This can be anything that
+you usually put inside your apps's \code{ui} function. If you're inserting
+multiple elements in one call, make sure to wrap them in either a
+\code{tagList()} or a \code{tags$div()} (the latter option has the
+advantage that you can give it an \code{id} to make it easier to
+reference or remove it later on). If you want to insert raw html, use
+\code{ui = HTML()}.}
+
+\item{multiple}{In case your selector matches more than one element,
+\code{multiple} determines whether Shiny should insert the UI object
+relative to all matched elements or just relative to the first
+matched element (default).}
+
+\item{immediate}{Whether the UI object should be immediately inserted into
+the app when you call \code{insertUI}, or whether Shiny should wait until
+all outputs have been updated and all observers have been run (default).}
+
+\item{session}{The shiny session within which to call \code{insertUI}.}
+}
+\description{
+Insert a UI object into the app.
+}
+\details{
+This function allows you to dynamically add an arbitrarily large UI
+object into your app, whenever you want, as many times as you want.
+Unlike \code{\link{renderUI}}, the UI generated with \code{insertUI}
+is not updatable as a whole: once it's created, it stays there. Each
+new call to \code{insertUI} creates more UI objects, in addition to
+the ones already there (all independent from one another). To
+update a part of the UI (ex: an input object), you must use the
+appropriate \code{render} function or a customized \code{reactive}
+function. To remove any part of your UI, use \code{\link{removeUI}}.
+}
+\examples{
+## Only run this example in interactive R sessions
+if (interactive()) {
+# Define UI
+ui <- fluidPage(
+  actionButton("add", "Add UI")
+)
+
+# Server logic
+server <- function(input, output, session) {
+  observeEvent(input$add, {
+    insertUI(
+      selector = "#add",
+      where = "afterEnd",
+      ui = textInput(paste0("txt", input$add),
+                     "Insert some text")
+    )
+  })
+}
+
+# Complete app with UI and server components
+shinyApp(ui, server)
+}
+}
+\seealso{
+\code{\link{removeUI}}
+}
+
diff --git a/man/invalidateLater.Rd b/man/invalidateLater.Rd
index 968122a..681cd6e 100644
--- a/man/invalidateLater.Rd
+++ b/man/invalidateLater.Rd
@@ -28,8 +28,15 @@ possible to stop this cycle by adding conditional logic that prevents the
 \code{invalidateLater} from being run.
 }
 \examples{
-\dontrun{
-shinyServer(function(input, output, session) {
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  sliderInput("n", "Number of observations", 2, 1000, 500),
+  plotOutput("plot")
+)
+
+server <- function(input, output, session) {
 
   observe({
     # Re-execute this reactive expression after 1000 milliseconds
@@ -46,11 +53,12 @@ shinyServer(function(input, output, session) {
   output$plot <- renderPlot({
     # Re-execute this reactive expression after 2000 milliseconds
     invalidateLater(2000)
-    hist(isolate(input$n))
+    hist(rnorm(isolate(input$n)))
   })
-})
 }
 
+shinyApp(ui, server)
+}
 }
 \seealso{
 \code{\link{reactiveTimer}} is a slightly less safe alternative.
diff --git a/man/isolate.Rd b/man/isolate.Rd
index ffe04c2..3bb4ff6 100644
--- a/man/isolate.Rd
+++ b/man/isolate.Rd
@@ -76,6 +76,5 @@ isolate(fun())
 
 # isolate also works if the reactive expression accesses values from the
 # input object, like input$x
-
 }
 
diff --git a/man/markRenderFunction.Rd b/man/markRenderFunction.Rd
index a642600..9f30fa4 100644
--- a/man/markRenderFunction.Rd
+++ b/man/markRenderFunction.Rd
@@ -4,7 +4,7 @@
 \alias{markRenderFunction}
 \title{Mark a function as a render function}
 \usage{
-markRenderFunction(uiFunc, renderFunc)
+markRenderFunction(uiFunc, renderFunc, outputArgs = list())
 }
 \arguments{
 \item{uiFunc}{A function that renders Shiny UI. Must take a single argument:
@@ -12,6 +12,13 @@ an output ID.}
 
 \item{renderFunc}{A function that is suitable for assigning to a Shiny output
 slot.}
+
+\item{outputArgs}{A list of arguments to pass to the \code{uiFunc}. Render
+functions should include \code{outputArgs = list()} in their own parameter
+list, and pass through the value to \code{markRenderFunction}, to allow
+app authors to customize outputs. (Currently, this is only supported for
+dynamically generated UIs, such as those created by Shiny code snippets
+embedded in R Markdown documents).}
 }
 \value{
 The \code{renderFunc} function, with annotations.
diff --git a/man/modalButton.Rd b/man/modalButton.Rd
new file mode 100644
index 0000000..4bc3f1b
--- /dev/null
+++ b/man/modalButton.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/modal.R
+\name{modalButton}
+\alias{modalButton}
+\title{Create a button for a modal dialog}
+\usage{
+modalButton(label, icon = NULL)
+}
+\arguments{
+\item{label}{The contents of the button or link--usually a text label, but
+you could also use any other HTML, like an image.}
+
+\item{icon}{An optional \code{\link{icon}} to appear on the button.}
+}
+\description{
+When clicked, a \code{modalButton} will dismiss the modal dialog.
+}
+\seealso{
+\code{\link{modalDialog}} for examples.
+}
+
diff --git a/man/modalDialog.Rd b/man/modalDialog.Rd
new file mode 100644
index 0000000..5bbfc5d
--- /dev/null
+++ b/man/modalDialog.Rd
@@ -0,0 +1,132 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/modal.R
+\name{modalDialog}
+\alias{modalDialog}
+\title{Create a modal dialog UI}
+\usage{
+modalDialog(..., title = NULL, footer = modalButton("Dismiss"),
+  size = c("m", "s", "l"), easyClose = FALSE, fade = TRUE)
+}
+\arguments{
+\item{...}{UI elements for the body of the modal dialog box.}
+
+\item{title}{An optional title for the dialog.}
+
+\item{footer}{UI for footer. Use \code{NULL} for no footer.}
+
+\item{size}{One of \code{"s"} for small, \code{"m"} (the default) for medium,
+or \code{"l"} for large.}
+
+\item{easyClose}{If \code{TRUE}, the modal dialog can be dismissed by
+clicking outside the dialog box, or be pressing the Escape key. If
+\code{FALSE} (the default), the modal dialog can't be dismissed in those
+ways; instead it must be dismissed by clicking on the dismiss button, or
+from a call to \code{\link{removeModal}} on the server.}
+
+\item{fade}{If \code{FALSE}, the modal dialog will have no fade-in animation
+(it will simply appear rather than fade in to view).}
+}
+\description{
+This creates the UI for a modal dialog, using Bootstrap's modal class. Modals
+are typically used for showing important messages, or for presenting UI that
+requires input from the user, such as a username and password input.
+}
+\examples{
+if (interactive()) {
+# Display an important message that can be dismissed only by clicking the
+# dismiss button.
+shinyApp(
+  ui = basicPage(
+    actionButton("show", "Show modal dialog")
+  ),
+  server = function(input, output) {
+    observeEvent(input$show, {
+      showModal(modalDialog(
+        title = "Important message",
+        "This is an important message!"
+      ))
+    })
+  }
+)
+
+
+# Display a message that can be dismissed by clicking outside the modal dialog,
+# or by pressing Esc.
+shinyApp(
+  ui = basicPage(
+    actionButton("show", "Show modal dialog")
+  ),
+  server = function(input, output) {
+    observeEvent(input$show, {
+      showModal(modalDialog(
+        title = "Somewhat important message",
+        "This is a somewhat important message.",
+        easyClose = TRUE,
+        footer = NULL
+      ))
+    })
+  }
+)
+
+
+# Display a modal that requires valid input before continuing.
+shinyApp(
+  ui = basicPage(
+    actionButton("show", "Show modal dialog"),
+    verbatimTextOutput("dataInfo")
+  ),
+
+  server = function(input, output) {
+    # reactiveValues object for storing current data set.
+    vals <- reactiveValues(data = NULL)
+
+    # Return the UI for a modal dialog with data selection input. If 'failed' is
+    # TRUE, then display a message that the previous value was invalid.
+    dataModal <- function(failed = FALSE) {
+      modalDialog(
+        textInput("dataset", "Choose data set",
+          placeholder = 'Try "mtcars" or "abc"'
+        ),
+        span('(Try the name of a valid data object like "mtcars", ',
+             'then a name of a non-existent object like "abc")'),
+        if (failed)
+          div(tags$b("Invalid name of data object", style = "color: red;")),
+
+        footer = tagList(
+          modalButton("Cancel"),
+          actionButton("ok", "OK")
+        )
+      )
+    }
+
+    # Show modal when button is clicked.
+    observeEvent(input$show, {
+      showModal(dataModal())
+    })
+
+    # When OK button is pressed, attempt to load the data set. If successful,
+    # remove the modal. If not show another modal, but this time with a failure
+    # message.
+    observeEvent(input$ok, {
+      # Check that data object exists and is data frame.
+      if (!is.null(input$dataset) && nzchar(input$dataset) &&
+          exists(input$dataset) && is.data.frame(get(input$dataset))) {
+        vals$data <- get(input$dataset)
+        removeModal()
+      } else {
+        showModal(dataModal(failed = TRUE))
+      }
+    })
+
+    # Display information about selected data
+    output$dataInfo <- renderPrint({
+      if (is.null(vals$data))
+        "No data selected"
+      else
+        summary(vals$data)
+    })
+  }
+)
+}
+}
+
diff --git a/man/navbarPage.Rd b/man/navbarPage.Rd
index c02ca3f..d293500 100644
--- a/man/navbarPage.Rd
+++ b/man/navbarPage.Rd
@@ -5,30 +5,37 @@
 \alias{navbarPage}
 \title{Create a page with a top level navigation bar}
 \usage{
-navbarPage(title, ..., id = NULL, position = c("static-top", "fixed-top",
-  "fixed-bottom"), header = NULL, footer = NULL, inverse = FALSE,
-  collapsible = FALSE, collapsable, fluid = TRUE, responsive = NULL,
-  theme = NULL, windowTitle = title)
+navbarPage(title, ..., id = NULL, selected = NULL,
+  position = c("static-top", "fixed-top", "fixed-bottom"), header = NULL,
+  footer = NULL, inverse = FALSE, collapsible = FALSE, collapsable,
+  fluid = TRUE, responsive = NULL, theme = NULL, windowTitle = title)
 
 navbarMenu(title, ..., icon = NULL)
 }
 \arguments{
 \item{title}{The title to display in the navbar}
 
-\item{...}{\code{\link{tabPanel}} elements to include in the page}
+\item{...}{\code{\link{tabPanel}} elements to include in the page. The
+\code{navbarMenu} function also accepts strings, which will be used as menu
+section headers. If the string is a set of dashes like \code{"----"} a
+horizontal separator will be displayed in the menu.}
 
 \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}}.}
 
+\item{selected}{The \code{value} (or, if none was supplied, the \code{title})
+of the tab that should be selected by default. If \code{NULL}, the first
+tab will be selected.}
+
 \item{position}{Determines whether the navbar should be displayed at the top
-of the page with normal scrolling behavior (\code{"static-top"}), pinned
-at the top (\code{"fixed-top"}), or pinned at the bottom
+of the page with normal scrolling behavior (\code{"static-top"}), pinned at
+the top (\code{"fixed-top"}), or pinned at the bottom
 (\code{"fixed-bottom"}). Note that using \code{"fixed-top"} or
 \code{"fixed-bottom"} will cause the navbar to overlay your body content,
-unless you add padding, e.g.:
-\code{tags$style(type="text/css", "body {padding-top: 70px;}")}}
+unless you add padding, e.g.: \code{tags$style(type="text/css", "body
+{padding-top: 70px;}")}}
 
 \item{header}{Tag or list of tags to display as a common header above all
 tabPanels.}
@@ -73,19 +80,21 @@ The \code{navbarMenu} function can be used to create an embedded
   example below).
 }
 \examples{
-shinyUI(navbarPage("App Title",
+navbarPage("App Title",
   tabPanel("Plot"),
   tabPanel("Summary"),
   tabPanel("Table")
-))
+)
 
-shinyUI(navbarPage("App Title",
+navbarPage("App Title",
   tabPanel("Plot"),
   navbarMenu("More",
     tabPanel("Summary"),
+    "----",
+    "Section header",
     tabPanel("Table")
   )
-))
+)
 }
 \seealso{
 \code{\link{tabPanel}}, \code{\link{tabsetPanel}},
diff --git a/man/navlistPanel.Rd b/man/navlistPanel.Rd
index 0d67cd6..a00b4bc 100644
--- a/man/navlistPanel.Rd
+++ b/man/navlistPanel.Rd
@@ -40,7 +40,7 @@ You can include headers within the \code{navlistPanel} by including
   doesn't support separators.
 }
 \examples{
-shinyUI(fluidPage(
+fluidPage(
 
   titlePanel("Application Title"),
 
@@ -50,7 +50,7 @@ shinyUI(fluidPage(
     tabPanel("Second"),
     tabPanel("Third")
   )
-))
+)
 }
 \seealso{
 \code{\link{tabPanel}}, \code{\link{updateNavlistPanel}}
diff --git a/man/numericInput.Rd b/man/numericInput.Rd
index 7b0d67f..4c506db 100644
--- a/man/numericInput.Rd
+++ b/man/numericInput.Rd
@@ -30,8 +30,18 @@ A numeric input control that can be added to a UI definition.
 Create an input control for entry of numeric values
 }
 \examples{
-numericInput("obs", "Observations:", 10,
-             min = 1, max = 100)
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  numericInput("obs", "Observations:", 10, min = 1, max = 100),
+  verbatimTextOutput("value")
+)
+server <- function(input, output) {
+  output$value <- renderText({ input$obs })
+}
+shinyApp(ui, server)
+}
 }
 \seealso{
 \code{\link{updateNumericInput}}
@@ -42,6 +52,7 @@ Other input.elements: \code{\link{actionButton}},
   \code{\link{dateRangeInput}}, \code{\link{fileInput}},
   \code{\link{passwordInput}}, \code{\link{radioButtons}},
   \code{\link{selectInput}}, \code{\link{sliderInput}},
-  \code{\link{submitButton}}, \code{\link{textInput}}
+  \code{\link{submitButton}}, \code{\link{textAreaInput}},
+  \code{\link{textInput}}
 }
 
diff --git a/man/observe.Rd b/man/observe.Rd
index 8cee255..a659b99 100644
--- a/man/observe.Rd
+++ b/man/observe.Rd
@@ -22,13 +22,14 @@ variable; to do so, it must be quoted with \code{quote()}.}
 
 \item{label}{A label for the observer, useful for debugging.}
 
-\item{suspended}{If \code{TRUE}, start the observer in a suspended state.
-If \code{FALSE} (the default), start in a non-suspended state.}
+\item{suspended}{If \code{TRUE}, start the observer in a suspended state. If
+\code{FALSE} (the default), start in a non-suspended state.}
 
 \item{priority}{An integer or numeric that controls the priority with which
-this observer should be executed. An observer with a given priority level
-will always execute sooner than all observers with a lower priority level.
-Positive, negative, and zero values are allowed.}
+this observer should be executed. A higher value means higher priority: an
+observer with a higher priority value will execute before all observers
+with lower priority values. Positive, negative, and zero values are
+allowed.}
 
 \item{domain}{See \link{domains}.}
 
@@ -94,8 +95,8 @@ they will never re-execute. In contrast, observers use eager evaluation; as
 soon as their dependencies change, they schedule themselves to re-execute.
 
 Starting with Shiny 0.10.0, observers are automatically destroyed by default
-when the \link[=domains]{domain} that owns them ends (e.g. when a Shiny session
-ends).
+when the \link[=domains]{domain} that owns them ends (e.g. when a Shiny
+session ends).
 }
 \examples{
 values <- reactiveValues(A=1)
@@ -114,6 +115,5 @@ obsD <- observe(expr_q, quoted = TRUE)
 # In a normal Shiny app, the web client will trigger flush events. If you
 # are at the console, you can force a flush with flushReact()
 shiny:::flushReact()
-
 }
 
diff --git a/man/observeEvent.Rd b/man/observeEvent.Rd
index f8ff0e8..889d3e7 100644
--- a/man/observeEvent.Rd
+++ b/man/observeEvent.Rd
@@ -149,7 +149,6 @@ if (interactive()) {
   }
   shinyApp(ui=ui, server=server)
 }
-
 }
 \seealso{
 \code{\link{actionButton}}
diff --git a/man/onBookmark.Rd b/man/onBookmark.Rd
new file mode 100644
index 0000000..3db81e8
--- /dev/null
+++ b/man/onBookmark.Rd
@@ -0,0 +1,209 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/bookmark-state.R
+\name{onBookmark}
+\alias{onBookmark}
+\alias{onBookmarked}
+\alias{onRestore}
+\alias{onRestored}
+\title{Add callbacks for Shiny session bookmarking events}
+\usage{
+onBookmark(fun, session = getDefaultReactiveDomain())
+
+onBookmarked(fun, session = getDefaultReactiveDomain())
+
+onRestore(fun, session = getDefaultReactiveDomain())
+
+onRestored(fun, session = getDefaultReactiveDomain())
+}
+\arguments{
+\item{fun}{A callback function which takes one argument.}
+
+\item{session}{A shiny session object.}
+}
+\description{
+These functions are for registering callbacks on Shiny session events. They
+should be called within an application's server function.
+
+\itemize{
+  \item \code{onBookmark} registers a function that will be called just
+    before Shiny bookmarks state.
+  \item \code{onBookmarked} registers a function that will be called just
+    after Shiny bookmarks state.
+  \item \code{onRestore} registers a function that will be called when a
+    session is restored, after the server function executes, but before all
+    other reactives, observers and render functions are run.
+  \item \code{onRestored} registers a function that will be called after a
+    session is restored. This is similar to \code{onRestore}, but it will be
+    called after all reactives, observers, and render functions run, and
+    after results are sent to the client browser. \code{onRestored}
+    callbacks can be useful for sending update messages to the client
+    browser.
+}
+}
+\details{
+All of these functions return a function which can be called with no
+arguments to cancel the registration.
+
+The callback function that is passed to these functions should take one
+argument, typically named "state" (for \code{onBookmark}, \code{onRestore},
+and \code{onRestored}) or "url" (for \code{onBookmarked}).
+
+For \code{onBookmark}, the state object has three relevant fields. The
+\code{values} field is an environment which can be used to save arbitrary
+values (see examples). If the state is being saved to disk (as opposed to
+being encoded in a URL), the \code{dir} field contains the name of a
+directory which can be used to store extra files. Finally, the state object
+has an \code{input} field, which is simply the application's \code{input}
+object. It can be read, but not modified.
+
+For \code{onRestore} and \code{onRestored}, the state object is a list. This
+list contains \code{input}, which is a named list of input values to restore,
+\code{values}, which is an environment containing arbitrary values that were
+saved in \code{onBookmark}, and \code{dir}, the name of the directory that
+the state is being restored from, and which could have been used to save
+extra files.
+
+For \code{onBookmarked}, the callback function receives a string with the
+bookmark URL. This callback function should be used to display UI in the
+client browser with the bookmark URL. If no callback function is registered,
+then Shiny will by default display a modal dialog with the bookmark URL.
+}
+\section{Modules}{
+
+
+  These callbacks may also be used in Shiny modules. When used this way, the
+  inputs and values will automatically be namespaced for the module, and the
+  callback functions registered for the module will only be able to see the
+  module's inputs and values.
+}
+\examples{
+## Only run these examples in interactive sessions
+if (interactive()) {
+
+# Basic use of onBookmark and onRestore: This app saves the time in its
+# arbitrary values, and restores that time when the app is restored.
+ui <- function(req) {
+  fluidPage(
+    textInput("txt", "Input text"),
+    bookmarkButton()
+  )
+}
+server <- function(input, output) {
+  onBookmark(function(state) {
+    savedTime <- as.character(Sys.time())
+    cat("Last saved at", savedTime, "\\n")
+    # state is a mutable reference object, and we can add arbitrary values to
+    # it.
+    state$values$time <- savedTime
+  })
+
+  onRestore(function(state) {
+    cat("Restoring from state bookmarked at", state$values$time, "\\n")
+  })
+}
+enableBookmarking("url")
+shinyApp(ui, server)
+
+
+
+ui <- function(req) {
+  fluidPage(
+    textInput("txt", "Input text"),
+    bookmarkButton()
+  )
+}
+server <- function(input, output, session) {
+  lastUpdateTime <- NULL
+
+  observeEvent(input$txt, {
+    updateTextInput(session, "txt",
+      label = paste0("Input text (Changed ", as.character(Sys.time()), ")")
+    )
+  })
+
+  onBookmark(function(state) {
+    # Save content to a file
+    messageFile <- file.path(state$dir, "message.txt")
+    cat(as.character(Sys.time()), file = messageFile)
+  })
+
+  onRestored(function(state) {
+    # Read the file
+    messageFile <- file.path(state$dir, "message.txt")
+    timeText <- readChar(messageFile, 1000)
+
+    # updateTextInput must be called in onRestored, as opposed to onRestore,
+    # because onRestored happens after the client browser is ready.
+    updateTextInput(session, "txt",
+      label = paste0("Input text (Changed ", timeText, ")")
+    )
+  })
+}
+# "server" bookmarking is needed for writing to disk.
+enableBookmarking("server")
+shinyApp(ui, server)
+
+
+# This app has a module, and both the module and the main app code have
+# onBookmark and onRestore functions which write and read state$values$hash. The
+# module's version of state$values$hash does not conflict with the app's version
+# of state$values$hash.
+#
+# A basic module that captializes text.
+capitalizerUI <- function(id) {
+  ns <- NS(id)
+  wellPanel(
+    h4("Text captializer module"),
+    textInput(ns("text"), "Enter text:"),
+    verbatimTextOutput(ns("out"))
+  )
+}
+capitalizerServer <- function(input, output, session) {
+  output$out <- renderText({
+    toupper(input$text)
+  })
+  onBookmark(function(state) {
+    state$values$hash <- digest::digest(input$text, "md5")
+  })
+  onRestore(function(state) {
+    if (identical(digest::digest(input$text, "md5"), state$values$hash)) {
+      message("Module's input text matches hash ", state$values$hash)
+    } else {
+      message("Module's input text does not match hash ", state$values$hash)
+    }
+  })
+}
+# Main app code
+ui <- function(request) {
+  fluidPage(
+    sidebarLayout(
+      sidebarPanel(
+        capitalizerUI("tc"),
+        textInput("text", "Enter text (not in module):"),
+        bookmarkButton()
+      ),
+      mainPanel()
+    )
+  )
+}
+server <- function(input, output, session) {
+  callModule(capitalizerServer, "tc")
+  onBookmark(function(state) {
+    state$values$hash <- digest::digest(input$text, "md5")
+  })
+  onRestore(function(state) {
+    if (identical(digest::digest(input$text, "md5"), state$values$hash)) {
+      message("App's input text matches hash ", state$values$hash)
+    } else {
+      message("App's input text does not match hash ", state$values$hash)
+    }
+  })
+}
+enableBookmarking(store = "url")
+shinyApp(ui, server)
+}
+}
+\seealso{
+enableBookmarking for general information on bookmarking.
+}
+
diff --git a/man/onFlush.Rd b/man/onFlush.Rd
new file mode 100644
index 0000000..964149a
--- /dev/null
+++ b/man/onFlush.Rd
@@ -0,0 +1,37 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/shiny.R
+\name{onFlush}
+\alias{onFlush}
+\alias{onFlushed}
+\alias{onSessionEnded}
+\title{Add callbacks for Shiny session events}
+\usage{
+onFlush(fun, once = TRUE, session = getDefaultReactiveDomain())
+
+onFlushed(fun, once = TRUE, session = getDefaultReactiveDomain())
+
+onSessionEnded(fun, session = getDefaultReactiveDomain())
+}
+\arguments{
+\item{fun}{A callback function.}
+
+\item{once}{Should the function be run once, and then cleared, or should it
+re-run each time the event occurs. (Only for \code{onFlush} and
+\code{onFlushed}.)}
+
+\item{session}{A shiny session object.}
+}
+\description{
+These functions are for registering callbacks on Shiny session events.
+\code{onFlush} registers a function that will be called before Shiny flushes
+the reactive system. \code{onFlushed} registers a function that will be
+called after Shiny flushes the reactive system. \code{onSessionEnded}
+registers a function to be called after the client has disconnected.
+}
+\details{
+These functions should be called within the application's server function.
+
+All of these functions return a function which can be called with no
+arguments to cancel the registration.
+}
+
diff --git a/man/pageWithSidebar.Rd b/man/pageWithSidebar.Rd
index 1ec80f2..cba5a99 100644
--- a/man/pageWithSidebar.Rd
+++ b/man/pageWithSidebar.Rd
@@ -26,7 +26,7 @@ along with \code{\link{sidebarLayout}} to implement a page with a sidebar.
 }
 \examples{
 # Define UI
-shinyUI(pageWithSidebar(
+pageWithSidebar(
 
   # Application title
   headerPanel("Hello Shiny!"),
@@ -44,7 +44,6 @@ shinyUI(pageWithSidebar(
   mainPanel(
     plotOutput("distPlot")
   )
-))
-
+)
 }
 
diff --git a/man/parseQueryString.Rd b/man/parseQueryString.Rd
index 6148a07..626188c 100644
--- a/man/parseQueryString.Rd
+++ b/man/parseQueryString.Rd
@@ -24,7 +24,7 @@ parseQueryString("?foo=1&bar=b\%20a\%20r")
 
 \dontrun{
 # Example of usage within a Shiny app
-shinyServer(function(input, output, session) {
+function(input, output, session) {
 
   output$queryText <- renderText({
     query <- parseQueryString(session$clientData$url_search)
@@ -40,7 +40,7 @@ shinyServer(function(input, output, session) {
     # Return a string with key-value pairs
     paste(names(query), query, sep = "=", collapse=", ")
   })
-})
+}
 }
 
 }
diff --git a/man/passwordInput.Rd b/man/passwordInput.Rd
index 92dab50..1009ff1 100644
--- a/man/passwordInput.Rd
+++ b/man/passwordInput.Rd
@@ -4,7 +4,8 @@
 \alias{passwordInput}
 \title{Create a password input control}
 \usage{
-passwordInput(inputId, label, value = "", width = NULL)
+passwordInput(inputId, label, value = "", width = NULL,
+  placeholder = NULL)
 }
 \arguments{
 \item{inputId}{The \code{input} slot that will be used to access the value.}
@@ -15,6 +16,10 @@ passwordInput(inputId, label, value = "", width = NULL)
 
 \item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
 see \code{\link{validateCssUnit}}.}
+
+\item{placeholder}{A character string giving the user a hint as to what can
+be entered into the control. Internet Explorer 8 and 9 do not support this
+option.}
 }
 \value{
 A text input control that can be added to a UI definition.
@@ -23,7 +28,22 @@ A text input control that can be added to a UI definition.
 Create an password control for entry of passwords.
 }
 \examples{
-passwordInput("password", "Password:")
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  passwordInput("password", "Password:"),
+  actionButton("go", "Go"),
+  verbatimTextOutput("value")
+)
+server <- function(input, output) {
+  output$value <- renderText({
+    req(input$go)
+    isolate(input$password)
+  })
+}
+shinyApp(ui, server)
+}
 }
 \seealso{
 \code{\link{updateTextInput}}
@@ -34,6 +54,7 @@ Other input.elements: \code{\link{actionButton}},
   \code{\link{dateRangeInput}}, \code{\link{fileInput}},
   \code{\link{numericInput}}, \code{\link{radioButtons}},
   \code{\link{selectInput}}, \code{\link{sliderInput}},
-  \code{\link{submitButton}}, \code{\link{textInput}}
+  \code{\link{submitButton}}, \code{\link{textAreaInput}},
+  \code{\link{textInput}}
 }
 
diff --git a/man/radioButtons.Rd b/man/radioButtons.Rd
index 83d6941..bf07086 100644
--- a/man/radioButtons.Rd
+++ b/man/radioButtons.Rd
@@ -38,11 +38,33 @@ Instead, consider having the first of your choices be \code{c("None selected"
 = "")}.
 }
 \examples{
-radioButtons("dist", "Distribution type:",
-             c("Normal" = "norm",
-               "Uniform" = "unif",
-               "Log-normal" = "lnorm",
-               "Exponential" = "exp"))
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  radioButtons("dist", "Distribution type:",
+               c("Normal" = "norm",
+                 "Uniform" = "unif",
+                 "Log-normal" = "lnorm",
+                 "Exponential" = "exp")),
+  plotOutput("distPlot")
+)
+
+server <- function(input, output) {
+  output$distPlot <- renderPlot({
+    dist <- switch(input$dist,
+                   norm = rnorm,
+                   unif = runif,
+                   lnorm = rlnorm,
+                   exp = rexp,
+                   rnorm)
+
+    hist(dist(500))
+  })
+}
+
+shinyApp(ui, server)
+}
 }
 \seealso{
 \code{\link{updateRadioButtons}}
@@ -53,6 +75,7 @@ Other input.elements: \code{\link{actionButton}},
   \code{\link{dateRangeInput}}, \code{\link{fileInput}},
   \code{\link{numericInput}}, \code{\link{passwordInput}},
   \code{\link{selectInput}}, \code{\link{sliderInput}},
-  \code{\link{submitButton}}, \code{\link{textInput}}
+  \code{\link{submitButton}}, \code{\link{textAreaInput}},
+  \code{\link{textInput}}
 }
 
diff --git a/man/reactive.Rd b/man/reactive.Rd
index 87ecb1c..889e6fa 100644
--- a/man/reactive.Rd
+++ b/man/reactive.Rd
@@ -66,6 +66,5 @@ reactiveD <- reactive(expr_q, quoted = TRUE)
 isolate(reactiveB())
 isolate(reactiveC())
 isolate(reactiveD())
-
 }
 
diff --git a/man/reactiveFileReader.Rd b/man/reactiveFileReader.Rd
index 72d2220..1ca102c 100644
--- a/man/reactiveFileReader.Rd
+++ b/man/reactiveFileReader.Rd
@@ -47,7 +47,7 @@ reactive values and reactive expressions.
 \examples{
 \dontrun{
 # Per-session reactive file reader
-shinyServer(function(input, output, session)) {
+function(input, output, session) {
   fileData <- reactiveFileReader(1000, session, 'data.csv', read.csv)
 
   output$data <- renderTable({
@@ -59,13 +59,12 @@ shinyServer(function(input, output, session)) {
 # the same reader, so read.csv only gets executed once no matter how many
 # user sessions are connected.
 fileData <- reactiveFileReader(1000, session, 'data.csv', read.csv)
-shinyServer(function(input, output, session)) {
+function(input, output, session) {
   output$data <- renderTable({
     fileData()
   })
 }
 }
-
 }
 \seealso{
 \code{\link{reactivePoll}}
diff --git a/man/reactivePoll.Rd b/man/reactivePoll.Rd
index 89bcb09..f13e3c4 100644
--- a/man/reactivePoll.Rd
+++ b/man/reactivePoll.Rd
@@ -57,16 +57,13 @@ will be executed in a reactive context; therefore, they may read reactive
 values and reactive expressions.
 }
 \examples{
-\dontrun{
 # Assume the existence of readTimestamp and readValue functions
-shinyServer(function(input, output, session) {
+function(input, output, session) {
   data <- reactivePoll(1000, session, readTimestamp, readValue)
   output$dataTable <- renderTable({
     data()
   })
-})
 }
-
 }
 \seealso{
 \code{\link{reactiveFileReader}}
diff --git a/man/reactiveTimer.Rd b/man/reactiveTimer.Rd
index 3f99d3d..d81a2e9 100644
--- a/man/reactiveTimer.Rd
+++ b/man/reactiveTimer.Rd
@@ -34,8 +34,15 @@ needed.
 See \code{\link{invalidateLater}} as a safer and simpler alternative.
 }
 \examples{
-\dontrun{
-shinyServer(function(input, output, session) {
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  sliderInput("n", "Number of observations", 2, 1000, 500),
+  plotOutput("plot")
+)
+
+server <- function(input, output) {
 
   # Anything that calls autoInvalidate will automatically invalidate
   # every 2 seconds.
@@ -56,11 +63,12 @@ shinyServer(function(input, output, session) {
   # input$n changes.
   output$plot <- renderPlot({
     autoInvalidate()
-    hist(isolate(input$n))
+    hist(rnorm(isolate(input$n)))
   })
-})
 }
 
+shinyApp(ui, server)
+}
 }
 \seealso{
 \code{\link{invalidateLater}}
diff --git a/man/reactiveValuesToList.Rd b/man/reactiveValuesToList.Rd
index a03ecf8..86028bb 100644
--- a/man/reactiveValuesToList.Rd
+++ b/man/reactiveValuesToList.Rd
@@ -28,6 +28,5 @@ reactiveValuesToList(values)
 # isolate() can also be used when calling from outside a reactive context (e.g.
 # at the console)
 isolate(reactiveValuesToList(values))
-
 }
 
diff --git a/man/removeUI.Rd b/man/removeUI.Rd
new file mode 100644
index 0000000..6dd0246
--- /dev/null
+++ b/man/removeUI.Rd
@@ -0,0 +1,67 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/insert-ui.R
+\name{removeUI}
+\alias{removeUI}
+\title{Remove UI objects}
+\usage{
+removeUI(selector, multiple = FALSE, immediate = FALSE,
+  session = getDefaultReactiveDomain())
+}
+\arguments{
+\item{selector}{A string that is accepted by jQuery's selector (i.e. the
+string \code{s} to be placed in a \code{$(s)} jQuery call). This selector
+will determine the element(s) to be removed. If you want to remove a
+Shiny input or output, note that many of these are wrapped in \code{div}s,
+so you may need to use a somewhat complex selector -- see the Examples below.
+(Alternatively, you could also wrap the inputs/outputs that you want to be
+able to remove easily in a \code{div} with an id.)}
+
+\item{multiple}{In case your selector matches more than one element,
+\code{multiple} determines whether Shiny should remove all the matched
+elements or just the first matched element (default).}
+
+\item{immediate}{Whether the element(s) should be immediately removed from
+the app when you call \code{removeUI}, or whether Shiny should wait until
+all outputs have been updated and all observers have been run (default).}
+
+\item{session}{The shiny session within which to call \code{removeUI}.}
+}
+\description{
+Remove a UI object from the app.
+}
+\details{
+This function allows you to remove any part of your UI. Once \code{removeUI}
+is executed on some element, it is gone forever.
+
+While it may be a particularly useful pattern to pair this with
+\code{\link{insertUI}} (to remove some UI you had previously inserted),
+there is no restriction on what you can use \code{removeUI} on. Any
+element that can be selected through a jQuery selector can be removed
+through this function.
+}
+\examples{
+## Only run this example in interactive R sessions
+if (interactive()) {
+# Define UI
+ui <- fluidPage(
+  actionButton("rmv", "Remove UI"),
+  textInput("txt", "This is no longer useful")
+)
+
+# Server logic
+server <- function(input, output, session) {
+  observeEvent(input$rmv, {
+    removeUI(
+      selector = "div:has(> #txt)"
+    )
+  })
+}
+
+# Complete app with UI and server components
+shinyApp(ui, server)
+}
+}
+\seealso{
+\code{\link{insertUI}}
+}
+
diff --git a/man/renderDataTable.Rd b/man/renderDataTable.Rd
index bab12eb..7033ef9 100644
--- a/man/renderDataTable.Rd
+++ b/man/renderDataTable.Rd
@@ -6,7 +6,7 @@
 \usage{
 renderDataTable(expr, options = NULL, searchDelay = 500,
   callback = "function(oTable) {}", escape = TRUE, env = parent.frame(),
-  quoted = FALSE)
+  quoted = FALSE, outputArgs = list())
 }
 \arguments{
 \item{expr}{An expression that returns a data frame or a matrix.}
@@ -32,6 +32,10 @@ third), or \code{c('Species', 'Sepal.Length')}.}
 
 \item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This
 is useful if you want to save an expression in a variable.}
+
+\item{outputArgs}{A list of arguments to be passed through to the implicit
+call to \code{\link{dataTableOutput}} when \code{renderDataTable} is used
+in an interactive R Markdown document.}
 }
 \description{
 Makes a reactive version of the given function that returns a data frame (or
diff --git a/man/renderImage.Rd b/man/renderImage.Rd
index e4b1e21..cb820ef 100644
--- a/man/renderImage.Rd
+++ b/man/renderImage.Rd
@@ -4,7 +4,8 @@
 \alias{renderImage}
 \title{Image file output}
 \usage{
-renderImage(expr, env = parent.frame(), quoted = FALSE, deleteFile = TRUE)
+renderImage(expr, env = parent.frame(), quoted = FALSE, deleteFile = TRUE,
+  outputArgs = list())
 }
 \arguments{
 \item{expr}{An expression that returns a list.}
@@ -18,6 +19,10 @@ is useful if you want to save an expression in a variable.}
 it is sent to the client browser? Generally speaking, if the image is a
 temp file generated within \code{func}, then this should be \code{TRUE};
 if the image is not a temp file, this should be \code{FALSE}.}
+
+\item{outputArgs}{A list of arguments to be passed through to the implicit
+call to \code{\link{imageOutput}} when \code{renderImage} is used in an
+interactive R Markdown document.}
 }
 \description{
 Renders a reactive image that is suitable for assigning to an \code{output}
@@ -40,9 +45,17 @@ The corresponding HTML output tag should be \code{div} or \code{img} and have
 the CSS class name \code{shiny-image-output}.
 }
 \examples{
-\dontrun{
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  sliderInput("n", "Number of observations", 2, 1000, 500),
+  plotOutput("plot1"),
+  plotOutput("plot2"),
+  plotOutput("plot3")
+)
 
-shinyServer(function(input, output, clientData) {
+server <- function(input, output, session) {
 
   # A plot of fixed size
   output$plot1 <- renderImage({
@@ -64,14 +77,14 @@ shinyServer(function(input, output, clientData) {
   output$plot2 <- renderImage({
     # Read plot2's width and height. These are reactive values, so this
     # expression will re-run whenever these values change.
-    width  <- clientData$output_plot2_width
-    height <- clientData$output_plot2_height
+    width  <- session$clientData$output_plot2_width
+    height <- session$clientData$output_plot2_height
 
     # A temp file to save the output.
     outfile <- tempfile(fileext='.png')
 
     png(outfile, width=width, height=height)
-    hist(rnorm(input$obs))
+    hist(rnorm(input$n))
     dev.off()
 
     # Return a list containing the filename
@@ -82,6 +95,8 @@ shinyServer(function(input, output, clientData) {
   }, deleteFile = TRUE)
 
   # Send a pre-rendered image, and don't delete the image after sending it
+  # NOTE: For this example to work, it would require files in a subdirectory
+  # named images/
   output$plot3 <- renderImage({
     # When input$n is 1, filename is ./images/image1.jpeg
     filename <- normalizePath(file.path('./images',
@@ -90,8 +105,9 @@ shinyServer(function(input, output, clientData) {
     # Return a list containing the filename
     list(src = filename)
   }, deleteFile = FALSE)
-})
+}
 
+shinyApp(ui, server)
 }
 }
 \seealso{
diff --git a/man/renderPlot.Rd b/man/renderPlot.Rd
index b318c77..a178f37 100644
--- a/man/renderPlot.Rd
+++ b/man/renderPlot.Rd
@@ -5,7 +5,8 @@
 \title{Plot Output}
 \usage{
 renderPlot(expr, width = "auto", height = "auto", res = 72, ...,
-  env = parent.frame(), quoted = FALSE, func = NULL)
+  env = parent.frame(), quoted = FALSE, execOnResize = FALSE,
+  outputArgs = list())
 }
 \arguments{
 \item{expr}{An expression that generates a plot.}
@@ -30,8 +31,16 @@ These can be used to set the width, height, background color, etc.}
 \item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This
 is useful if you want to save an expression in a variable.}
 
-\item{func}{A function that generates a plot (deprecated; use \code{expr}
-instead).}
+\item{execOnResize}{If \code{FALSE} (the default), then when a plot is
+resized, Shiny will \emph{replay} the plot drawing commands with
+\code{\link[grDevices]{replayPlot}()} instead of re-executing \code{expr}.
+This can result in faster plot redrawing, but there may be rare cases where
+it is undesirable. If you encounter problems when resizing a plot, you can
+have Shiny re-execute the code on resize by setting this to \code{TRUE}.}
+
+\item{outputArgs}{A list of arguments to be passed through to the implicit
+call to \code{\link{plotOutput}} when \code{renderPlot} is used in an
+interactive R Markdown document.}
 }
 \description{
 Renders a reactive plot that is suitable for assigning to an \code{output}
diff --git a/man/renderPrint.Rd b/man/renderPrint.Rd
index 98dd93c..0cde079 100644
--- a/man/renderPrint.Rd
+++ b/man/renderPrint.Rd
@@ -4,8 +4,8 @@
 \alias{renderPrint}
 \title{Printable Output}
 \usage{
-renderPrint(expr, env = parent.frame(), quoted = FALSE, func = NULL,
-  width = getOption("width"))
+renderPrint(expr, env = parent.frame(), quoted = FALSE,
+  width = getOption("width"), outputArgs = list())
 }
 \arguments{
 \item{expr}{An expression that may print output and/or return a printable R
@@ -13,12 +13,14 @@ object.}
 
 \item{env}{The environment in which to evaluate \code{expr}.}
 
-\item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This}
-
-\item{func}{A function that may print output and/or return a printable R
-object (deprecated; use \code{expr} instead).}
+\item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This
+is useful if you want to save an expression in a variable.}
 
 \item{width}{The value for \code{\link{options}('width')}.}
+
+\item{outputArgs}{A list of arguments to be passed through to the implicit
+call to \code{\link{verbatimTextOutput}} when \code{renderPrint} is used
+in an interactive R Markdown document.}
 }
 \description{
 Makes a reactive version of the given function that captures any printed
diff --git a/man/renderTable.Rd b/man/renderTable.Rd
index 5f2170b..731502e 100644
--- a/man/renderTable.Rd
+++ b/man/renderTable.Rd
@@ -4,29 +4,70 @@
 \alias{renderTable}
 \title{Table Output}
 \usage{
-renderTable(expr, ..., env = parent.frame(), quoted = FALSE, func = NULL)
+renderTable(expr, striped = FALSE, hover = FALSE, bordered = FALSE,
+  spacing = c("s", "xs", "m", "l"), width = "auto", align = NULL,
+  rownames = FALSE, colnames = TRUE, digits = NULL, na = "NA", ...,
+  env = parent.frame(), quoted = FALSE, outputArgs = list())
 }
 \arguments{
 \item{expr}{An expression that returns an R object that can be used with
 \code{\link[xtable]{xtable}}.}
 
-\item{...}{Arguments to be passed through to \code{\link[xtable]{xtable}} and
-\code{\link[xtable]{print.xtable}}.}
+\item{striped, hover, bordered}{Logicals: if \code{TRUE}, apply the
+corresponding Bootstrap table format to the output table.}
+
+\item{spacing}{The spacing between the rows of the table (\code{xs}
+stands for "extra small", \code{s} for "small", \code{m} for "medium"
+and \code{l} for "large").}
+
+\item{width}{Table width. Must be a valid CSS unit (like "100%", "400px",
+"auto") or a number, which will be coerced to a string and
+have "px" appended.}
+
+\item{align}{A string that specifies the column alignment. If equal to
+\code{'l'}, \code{'c'} or \code{'r'}, then all columns will be,
+respectively, left-, center- or right-aligned. Otherwise, \code{align}
+must have the same number of characters as the resulting table (if
+\code{rownames = TRUE}, this will be equal to \code{ncol()+1}), with
+the \emph{i}-th character specifying the alignment for the
+\emph{i}-th column (besides \code{'l'}, \code{'c'} and
+\code{'r'}, \code{'?'} is also permitted - \code{'?'} is a placeholder
+for that particular column, indicating that it should keep its default
+alignment). If \code{NULL}, then all numeric/integer columns (including
+the row names, if they are numbers) will be right-aligned and
+everything else will be left-aligned (\code{align = '?'} produces the
+same result).}
+
+\item{rownames, colnames}{Logicals: include rownames? include colnames
+(column headers)?}
+
+\item{digits}{An integer specifying the number of decimal places for
+the numeric columns (this will not apply to columns with an integer
+class). If \code{digits} is set to a negative value, then the numeric
+columns will be displayed in scientific format with a precision of
+\code{abs(digits)} digits.}
+
+\item{na}{The string to use in the table cells whose values are missing
+(i.e. they either evaluate to \code{NA} or \code{NaN}).}
+
+\item{...}{Arguments to be passed through to \code{\link[xtable]{xtable}}
+and \code{\link[xtable]{print.xtable}}.}
 
 \item{env}{The environment in which to evaluate \code{expr}.}
 
-\item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This
-is useful if you want to save an expression in a variable.}
+\item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})?
+This is useful if you want to save an expression in a variable.}
 
-\item{func}{A function that returns an R object that can be used with
-\code{\link[xtable]{xtable}} (deprecated; use \code{expr} instead).}
+\item{outputArgs}{A list of arguments to be passed through to the
+implicit call to \code{\link{tableOutput}} when \code{renderTable} is
+used in an interactive R Markdown document.}
 }
 \description{
 Creates a reactive table that is suitable for assigning to an \code{output}
 slot.
 }
 \details{
-The corresponding HTML output tag should be \code{div} and have the CSS class
-name \code{shiny-html-output}.
+The corresponding HTML output tag should be \code{div} and have the CSS
+class name \code{shiny-html-output}.
 }
 
diff --git a/man/renderText.Rd b/man/renderText.Rd
index 3dafba0..b70e32b 100644
--- a/man/renderText.Rd
+++ b/man/renderText.Rd
@@ -4,7 +4,8 @@
 \alias{renderText}
 \title{Text Output}
 \usage{
-renderText(expr, env = parent.frame(), quoted = FALSE, func = NULL)
+renderText(expr, env = parent.frame(), quoted = FALSE,
+  outputArgs = list())
 }
 \arguments{
 \item{expr}{An expression that returns an R object that can be used as an
@@ -15,8 +16,9 @@ argument to \code{cat}.}
 \item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This
 is useful if you want to save an expression in a variable.}
 
-\item{func}{A function that returns an R object that can be used as an
-argument to \code{cat}.(deprecated; use \code{expr} instead).}
+\item{outputArgs}{A list of arguments to be passed through to the implicit
+call to \code{\link{textOutput}} when \code{renderText} is used in an
+interactive R Markdown document.}
 }
 \description{
 Makes a reactive version of the given function that also uses
diff --git a/man/renderUI.Rd b/man/renderUI.Rd
index 921561e..14e670b 100644
--- a/man/renderUI.Rd
+++ b/man/renderUI.Rd
@@ -4,7 +4,7 @@
 \alias{renderUI}
 \title{UI Output}
 \usage{
-renderUI(expr, env = parent.frame(), quoted = FALSE, func = NULL)
+renderUI(expr, env = parent.frame(), quoted = FALSE, outputArgs = list())
 }
 \arguments{
 \item{expr}{An expression that returns a Shiny tag object, \code{\link{HTML}},
@@ -15,8 +15,9 @@ or a list of such objects.}
 \item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This
 is useful if you want to save an expression in a variable.}
 
-\item{func}{A function that returns a Shiny tag object, \code{\link{HTML}},
-or a list of such objects (deprecated; use \code{expr} instead).}
+\item{outputArgs}{A list of arguments to be passed through to the implicit
+call to \code{\link{uiOutput}} when \code{renderUI} is used in an
+interactive R Markdown document.}
 }
 \description{
 \bold{Experimental feature.} Makes a reactive version of a function that
@@ -27,13 +28,24 @@ 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 <- renderUI({
-    list(
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  uiOutput("moreControls")
+)
 
+server <- function(input, output) {
+  output$moreControls <- renderUI({
+    tagList(
+      sliderInput("n", "N", 1, 1000, 500),
+      textInput("label", "Label")
     )
   })
 }
+shinyApp(ui, server)
+}
+
 }
 \seealso{
 conditionalPanel
diff --git a/man/repeatable.Rd b/man/repeatable.Rd
index 165f1ac..5e49cf9 100644
--- a/man/repeatable.Rd
+++ b/man/repeatable.Rd
@@ -31,6 +31,5 @@ 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
-
 }
 
diff --git a/man/req.Rd b/man/req.Rd
index 87df994..8e8f9d5 100644
--- a/man/req.Rd
+++ b/man/req.Rd
@@ -1,13 +1,22 @@
 % Generated by roxygen2: do not edit by hand
 % Please edit documentation in R/utils.R
 \name{req}
+\alias{isTruthy}
 \alias{req}
 \title{Check for required values}
 \usage{
-req(...)
+req(..., cancelOutput = FALSE)
+
+isTruthy(x)
 }
 \arguments{
 \item{...}{Values to check for truthiness.}
+
+\item{cancelOutput}{If \code{TRUE} and an output is being evaluated, stop
+processing as usual but instead of clearing the output, leave it in
+whatever state it happens to be in.}
+
+\item{x}{An expression whose truthiness value we want to determine}
 }
 \value{
 The first value that was passed in.
@@ -82,5 +91,53 @@ always work around them. Since \code{FALSE} is falsy, you can simply provide
 the results of your own checks to \code{req}:
 
 \code{req(input$a != 0)}
+
+\strong{Using \code{req(FALSE)}}
+
+You can use \code{req(FALSE)} (i.e. no condition) if you've already performed
+all the checks you needed to by that point and just want to stop the reactive
+chain now. There is no advantange to this, except perhaps ease of readibility
+if you have a complicated condition to check for (or perhaps if you'd like to
+divide your condition into nested \code{if} statements).
+
+\strong{Using \code{cancelOutput = TRUE}}
+
+When \code{req(..., cancelOutput = TRUE)} is used, the "silent" exception is
+also raised, but it is treated slightly differently if one or more outputs are
+currently being evaluated. In those cases, the reactive chain does not proceed
+or update, but the output(s) are left is whatever state they happen to be in
+(whatever was their last valid state).
+
+Note that this is always going to be the case if
+this is used inside an output context (e.g. \code{output$txt <- ...}). It may
+or may not be the case if it is used inside a non-output context (e.g.
+\code{\link{reactive}}, \code{\link{observe}} or \code{\link{observeEvent}})
+-- depending on whether or not there is an \code{output$...} that is triggered
+as a result of those calls. See the examples below for concrete scenarios.
+}
+\examples{
+## Only run examples in interactive R sessions
+if (interactive()) {
+  ui <- fluidPage(
+    textInput('data', 'Enter a dataset from the "datasets" package', 'cars'),
+    p('(E.g. "cars", "mtcars", "pressure", "faithful")'), hr(),
+    tableOutput('tbl')
+  )
+
+  server <- function(input, output) {
+    output$tbl <- renderTable({
+
+      ## to require that the user types something, use: `req(input$data)`
+      ## but better: require that input$data is valid and leave the last
+      ## valid table up
+      req(exists(input$data, "package:datasets", inherits = FALSE),
+          cancelOutput = TRUE)
+
+      head(get(input$data, "package:datasets", inherits = FALSE))
+    })
+  }
+
+  shinyApp(ui, server)
+}
 }
 
diff --git a/man/restoreInput.Rd b/man/restoreInput.Rd
new file mode 100644
index 0000000..8ceb795
--- /dev/null
+++ b/man/restoreInput.Rd
@@ -0,0 +1,18 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/bookmark-state.R
+\name{restoreInput}
+\alias{restoreInput}
+\title{Restore an input value}
+\usage{
+restoreInput(id, default)
+}
+\arguments{
+\item{id}{Name of the input value to restore.}
+
+\item{default}{A default value to use, if there's no value to restore.}
+}
+\description{
+This restores an input value from the current restore context. It should be
+called early on inside of input functions (like \code{\link{textInput}}).
+}
+
diff --git a/man/runGadget.Rd b/man/runGadget.Rd
index ff8dd12..a3bfcf7 100644
--- a/man/runGadget.Rd
+++ b/man/runGadget.Rd
@@ -48,6 +48,5 @@ runGadget(ui, server)
 # ...or as a single app object
 runGadget(shinyApp(ui, server))
 }
-
 }
 
diff --git a/man/runUrl.Rd b/man/runUrl.Rd
index b40b99d..05d5860 100644
--- a/man/runUrl.Rd
+++ b/man/runUrl.Rd
@@ -55,7 +55,7 @@ respectively.
 }
 \examples{
 ## Only run this example in interactive R sessions
-  if (interactive()) {
+if (interactive()) {
   runUrl('https://github.com/rstudio/shiny_example/archive/master.tar.gz')
 
   # Can run an app from a subdirectory in the archive
diff --git a/man/safeError.Rd b/man/safeError.Rd
new file mode 100644
index 0000000..d55fc89
--- /dev/null
+++ b/man/safeError.Rd
@@ -0,0 +1,82 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/utils.R
+\name{safeError}
+\alias{safeError}
+\title{Declare an error safe for the user to see}
+\usage{
+safeError(error)
+}
+\arguments{
+\item{error}{Either an "error" object or a "character" object (string).
+In the latter case, the string will become the message of the error
+returned by \code{safeError}.}
+}
+\value{
+An "error" object
+}
+\description{
+This should be used when you want to let the user see an error
+message even if the default is to sanitize all errors. If you have an
+error \code{e} and call \code{stop(safeError(e))}, then Shiny will
+ignore the value of \code{getOption("shiny.sanitize.errors")} and always
+display the error in the app itself.
+}
+\details{
+An error generated by \code{safeError} has priority over all
+other Shiny errors. This can be dangerous. For example, if you have set
+\code{options(shiny.sanitize.errors = TRUE)}, then by default all error
+messages are omitted in the app, and replaced by a generic error message.
+However, this does not apply to \code{safeError}: whatever you pass
+through \code{error} will be displayed to the user. So, this should only
+be used when you are sure that your error message does not contain any
+sensitive information. In those situations, \code{safeError} can make
+your users' lives much easier by giving them a hint as to where the
+error occurred.
+}
+\examples{
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+# uncomment the desired line to experiment with shiny.sanitize.errors
+# options(shiny.sanitize.errors = TRUE)
+# options(shiny.sanitize.errors = FALSE)
+
+# Define UI
+ui <- fluidPage(
+  textInput('number', 'Enter your favorite number from 1 to 10', '5'),
+  textOutput('normalError'),
+  textOutput('safeError')
+)
+
+# Server logic
+server <- function(input, output) {
+  output$normalError <- renderText({
+    number <- input$number
+    if (number \%in\% 1:10) {
+      return(paste('You chose', number, '!'))
+    } else {
+      stop(
+        paste(number, 'is not a number between 1 and 10')
+      )
+    }
+  })
+  output$safeError <- renderText({
+    number <- input$number
+    if (number \%in\% 1:10) {
+      return(paste('You chose', number, '!'))
+    } else {
+      stop(safeError(
+        paste(number, 'is not a number between 1 and 10')
+      ))
+    }
+  })
+}
+
+# Complete app with UI and server components
+shinyApp(ui, server)
+}
+}
+\seealso{
+\code{\link{shiny-options}}
+}
+
diff --git a/man/selectInput.Rd b/man/selectInput.Rd
index 3963a2f..eb41a7c 100644
--- a/man/selectInput.Rd
+++ b/man/selectInput.Rd
@@ -70,10 +70,25 @@ The selectize input created from \code{selectizeInput()} allows
   \code{selectInput(..., selectize = FALSE)}.
 }
 \examples{
-selectInput("variable", "Variable:",
-            c("Cylinders" = "cyl",
-              "Transmission" = "am",
-              "Gears" = "gear"))
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  selectInput("variable", "Variable:",
+              c("Cylinders" = "cyl",
+                "Transmission" = "am",
+                "Gears" = "gear")),
+  tableOutput("data")
+)
+
+server <- function(input, output) {
+  output$data <- renderTable({
+    mtcars[, c("mpg", input$variable), drop = FALSE]
+  }, rownames = TRUE)
+}
+
+shinyApp(ui, server)
+}
 }
 \seealso{
 \code{\link{updateSelectInput}}
@@ -84,6 +99,7 @@ Other input.elements: \code{\link{actionButton}},
   \code{\link{dateRangeInput}}, \code{\link{fileInput}},
   \code{\link{numericInput}}, \code{\link{passwordInput}},
   \code{\link{radioButtons}}, \code{\link{sliderInput}},
-  \code{\link{submitButton}}, \code{\link{textInput}}
+  \code{\link{submitButton}}, \code{\link{textAreaInput}},
+  \code{\link{textInput}}
 }
 
diff --git a/man/session.Rd b/man/session.Rd
index b999c3d..50ef6ea 100644
--- a/man/session.Rd
+++ b/man/session.Rd
@@ -4,6 +4,18 @@
 \alias{session}
 \title{Session object}
 \value{
+\item{allowReconnect(value)}{
+  If \code{value} is \code{TRUE} and run in a hosting environment (Shiny
+  Server or Connect) with reconnections enabled,  then when the session ends
+  due to the network connection closing, the client will attempt to
+  reconnect to the server. If a reconnection is successful, the browser will
+  send all the current input values to the new session on the server, and
+  the server will recalculate any outputs and send them back to the client.
+  If \code{value} is \code{FALSE}, reconnections will be disabled (this is
+  the default state). If \code{"force"}, then the client browser will always
+  attempt to reconnect. The only reason to use \code{"force"} is for testing
+  on a local connection (without Shiny Server or Connect).
+}
 \item{clientData}{
   A \code{\link{reactiveValues}} object that contains information about the client.
   \itemize{
@@ -38,6 +50,11 @@
 \item{isClosed()}{A function that returns \code{TRUE} if the client has
   disconnected.
 }
+\item{ns(id)}{
+  Server-side version of \code{ns <- \link{NS}(id)}. If bare IDs need to be
+  explicitly namespaced for the current module, \code{session$ns("name")}
+  will return the fully-qualified ID.
+}
 \item{onEnded(callback)}{
   Synonym for \code{onSessionEnded}.
 }
@@ -85,6 +102,10 @@
   This is the request that was used to initiate the websocket connection
   (as opposed to the request that downloaded the web page for the app).
 }
+\item{resetBrush(brushId)}{
+  Resets/clears the brush with the given \code{brushId}, if it exists on
+  any \code{imageOutput} or \code{plotOutput} in the app.
+}
 \item{sendCustomMessage(type, message)}{
   Sends a custom message to the web page. \code{type} must be a
   single-element character vector giving the type of message, while
@@ -97,6 +118,12 @@
   \code{addCustomMessageHandler} will be invoked each time
   \code{sendCustomMessage} is called on the server.
 }
+\item{sendBinaryMessage(type, message)}{
+  Similar to \code{sendCustomMessage}, but the message must be a raw vector
+  and the registration method on the client is
+  \code{Shiny.addBinaryMessageHandler(type, function(message){...})}. The
+  message argument on the client will be a \href{https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView}{DataView}.
+}
 \item{sendInputMessage(inputId, message)}{
   Sends a message to an input on the session's client web page; if the input
   is present and bound on the page at the time the message is received, then
@@ -105,10 +132,41 @@
   from Shiny apps, but through friendlier wrapper functions like
   \code{\link{updateTextInput}}.
 }
-\item{ns(id)}{
-  Server-side version of \code{ns <- \link{NS}(id)}. If bare IDs need to be
-  explicitly namespaced for the current module, \code{session$ns("name")}
-  will return the fully-qualified ID.
+\item{setBookmarkExclude(names)}{
+  Set input names to be excluded from bookmarking.
+}
+\item{getBookmarkExclude()}{
+  Returns the set of input names to be excluded from bookmarking.
+}
+\item{onBookmark(fun)}{
+  Registers a function that will be called just before bookmarking state.
+}
+\item{onBookmarked(fun)}{
+  Registers a function that will be called just after bookmarking state.
+}
+\item{onRestore(fun)}{
+  Registers a function that will be called when a session is restored, before
+  all other reactives, observers, and render functions are run.
+}
+\item{onRestored(fun)}{
+  Registers a function that will be called when a session is restored, after
+  all other reactives, observers, and render functions are run.
+}
+\item{doBookmark()}{
+  Do bookmarking and invoke the onBookmark and onBookmarked callback functions.
+}
+\item{exportTestValues()}{
+  Registers expressions for export in test mode, available at the test
+  endpoint URL.
+}
+\item{getTestEndpointUrl(inputs=TRUE, outputs=TRUE, exports=TRUE,
+  format="rds")}{
+  Returns a URL for the test endpoint. Only has an effect when the
+  \code{shiny.testmode} option is set to TRUE. For the inputs, outputs, and
+  exports arguments, TRUE means to return all of these values. It is also
+  possible to specify by name which values to return by providing a
+  character vector, as in \code{inputs=c("x", "y")}. The format can be
+  "rds" or "json".
 }
 }
 \description{
diff --git a/man/setBookmarkExclude.Rd b/man/setBookmarkExclude.Rd
new file mode 100644
index 0000000..b819882
--- /dev/null
+++ b/man/setBookmarkExclude.Rd
@@ -0,0 +1,29 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/bookmark-state.R
+\name{setBookmarkExclude}
+\alias{setBookmarkExclude}
+\title{Exclude inputs from bookmarking}
+\usage{
+setBookmarkExclude(names = character(0),
+  session = getDefaultReactiveDomain())
+}
+\arguments{
+\item{names}{A character vector containing names of inputs to exclude from
+bookmarking.}
+
+\item{session}{A shiny session object.}
+}
+\description{
+This function tells Shiny which inputs should be excluded from bookmarking.
+It should be called from inside the application's server function.
+}
+\details{
+This function can also be called from a module's server function, in which
+case it will exclude inputs with the specified names, from that module. It
+will not affect inputs from other modules or from the top level of the Shiny
+application.
+}
+\seealso{
+\code{\link{enableBookmarking}} for examples.
+}
+
diff --git a/man/shiny-options.Rd b/man/shiny-options.Rd
index 70c2e3f..e0c93d5 100644
--- a/man/shiny-options.Rd
+++ b/man/shiny-options.Rd
@@ -13,9 +13,12 @@ be set with (for example) \code{options(shiny.trace=TRUE)}.
     when an app is run. See \code{\link{runApp}} for more information.}
   \item{shiny.port}{A port number that Shiny will listen on. See
     \code{\link{runApp}} for more information.}
-  \item{shiny.trace}{If \code{TRUE}, all of the messages sent between the R
-    server and the web browser client will be printed on the console. This
-    is useful for debugging.}
+  \item{shiny.trace}{Print messages sent between the R server and the web
+    browser client to the R console. This is useful for debugging. Possible
+    values are \code{"send"} (only print messages sent to the client),
+    \code{"recv"} (only print messages received by the server), \code{TRUE}
+    (print all messages), or \code{FALSE} (default; don't print any of these
+    messages).}
   \item{shiny.autoreload}{If \code{TRUE} when a Shiny app is launched, the
     app directory will be continually monitored for changes to files that
     have the extensions: r, htm, html, js, css, png, jpg, jpeg, gif. If any
@@ -27,10 +30,10 @@ Since monitoring for changes is expensive (we simply poll for last
 
 You can customize the file patterns Shiny will monitor by setting the
     shiny.autoreload.pattern option. For example, to monitor only ui.R:
-    \code{option(shiny.autoreload.pattern = glob2rx("ui.R"))}
+    \code{options(shiny.autoreload.pattern = glob2rx("ui.R"))}
 
 The default polling interval is 500 milliseconds. You can change this
-    by setting e.g. \code{option(shiny.autoreload.interval = 2000)} (every
+    by setting e.g. \code{options(shiny.autoreload.interval = 2000)} (every
     two seconds).}
   \item{shiny.reactlog}{If \code{TRUE}, enable logging of reactive events,
     which can be viewed later with the \code{\link{showReactLog}} function.
@@ -70,6 +73,17 @@ The default polling interval is 500 milliseconds. You can change this
     an arguably more intuitive arrangement for casual R users, as the name
     of a function appears next to the srcref where it is defined, rather than
     where it is currently being called from.}
+  \item{shiny.sanitize.errors}{If \code{TRUE}, then normal errors (i.e.
+    errors not wrapped in \code{safeError}) won't show up in the app; a simple
+    generic error message is printed instead (the error and strack trace printed
+    to the console remain unchanged). The default is \code{FALSE} (unsanitized
+    errors).If you want to sanitize errors in general, but you DO want a
+    particular error \code{e} to get displayed to the user, then set this option
+    to \code{TRUE} and use \code{stop(safeError(e))} for errors you want the
+    user to see.}
+  \item{shiny.testmode}{If \code{TRUE}, then enable features for testing Shiny
+    applications. If \code{FALSE} (the default), do not enable those features.
+  }
 }
 }
 
diff --git a/man/shinyApp.Rd b/man/shinyApp.Rd
index ab3bea5..81e9d66 100644
--- a/man/shinyApp.Rd
+++ b/man/shinyApp.Rd
@@ -14,7 +14,7 @@
 \title{Create a Shiny app object}
 \usage{
 shinyApp(ui = NULL, server = NULL, onStart = NULL, options = list(),
-  uiPattern = "/")
+  uiPattern = "/", enableBookmarking = NULL)
 
 shinyAppDir(appDir, options = list())
 
@@ -53,6 +53,13 @@ request to determine whether the \code{ui} should be used to handle the
 request. Note that the entire request path must match the regular
 expression in order for the match to be considered successful.}
 
+\item{enableBookmarking}{Can be one of \code{"url"}, \code{"server"}, or
+\code{"disable"}. This is equivalent to calling the
+\code{\link{enableBookmarking}()} function just before calling
+\code{shinyApp()}. With the default value (\code{NULL}), the app will
+respect the setting from any previous calls to \code{enableBookmarking()}.
+See \code{\link{enableBookmarking}} for more information.}
+
 \item{appDir}{Path to directory that contains a Shiny app (i.e. a server.R
 file and either ui.R or www/index.html)}
 
@@ -109,6 +116,5 @@ if (interactive()) {
 
   runApp(app)
 }
-
 }
 
diff --git a/man/shinyOptions.Rd b/man/shinyOptions.Rd
new file mode 100644
index 0000000..4a9f5b3
--- /dev/null
+++ b/man/shinyOptions.Rd
@@ -0,0 +1,39 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/shiny-options.R
+\name{getShinyOption}
+\alias{getShinyOption}
+\alias{shinyOptions}
+\title{Get or set Shiny options}
+\usage{
+getShinyOption(name, default = NULL)
+
+shinyOptions(...)
+}
+\arguments{
+\item{name}{Name of an option to get.}
+
+\item{default}{Value to be returned if the option is not currently set.}
+
+\item{...}{Options to set, with the form \code{name = value}.}
+}
+\description{
+\code{getShinyOption} retrieves the value of a Shiny option.
+\code{shinyOptions} sets the value of Shiny options; it can also be used to
+return a list of all currently-set Shiny options.
+}
+\details{
+There is a global option set, which is available by default. When a Shiny
+application is run with \code{\link{runApp}}, that option set is duplicated
+and the new option set is available for getting or setting values. If options
+are set from global.R, app.R, ui.R, or server.R, or if they are set from
+inside the server function, then the options will be scoped to the
+application. When the application exits, the new option set is discarded and
+the global option set is restored.
+}
+\examples{
+\dontrun{
+shinyOptions(myOption = 10)
+getShinyOption("myOption")
+}
+}
+
diff --git a/man/shinyServer.Rd b/man/shinyServer.Rd
index ba21ead..5bcf4df 100644
--- a/man/shinyServer.Rd
+++ b/man/shinyServer.Rd
@@ -53,6 +53,5 @@ function(input, output, session) {
   })
 }
 }
-
 }
 
diff --git a/man/showBookmarkUrlModal.Rd b/man/showBookmarkUrlModal.Rd
new file mode 100644
index 0000000..530c74a
--- /dev/null
+++ b/man/showBookmarkUrlModal.Rd
@@ -0,0 +1,19 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/bookmark-state.R
+\name{showBookmarkUrlModal}
+\alias{showBookmarkUrlModal}
+\title{Display a modal dialog for bookmarking}
+\usage{
+showBookmarkUrlModal(url)
+}
+\arguments{
+\item{url}{A URL to show in the modal dialog.}
+}
+\description{
+This is a wrapper function for \code{\link{urlModal}} that is automatically
+called if an application is bookmarked but no other \code{\link{onBookmark}}
+callback was set. It displays a modal dialog with the bookmark URL, along
+with a subtitle that is appropriate for the type of bookmarking used ("url"
+or "server").
+}
+
diff --git a/man/showModal.Rd b/man/showModal.Rd
new file mode 100644
index 0000000..0332036
--- /dev/null
+++ b/man/showModal.Rd
@@ -0,0 +1,25 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/modal.R
+\name{showModal}
+\alias{removeModal}
+\alias{showModal}
+\title{Show or remove a modal dialog}
+\usage{
+showModal(ui, session = getDefaultReactiveDomain())
+
+removeModal(session = getDefaultReactiveDomain())
+}
+\arguments{
+\item{ui}{UI content to show in the modal.}
+
+\item{session}{The \code{session} object passed to function given to
+\code{shinyServer}.}
+}
+\description{
+This causes a modal dialog to be displayed in the client browser, and is
+typically used with \code{\link{modalDialog}}.
+}
+\seealso{
+\code{\link{modalDialog}} for examples.
+}
+
diff --git a/man/showNotification.Rd b/man/showNotification.Rd
new file mode 100644
index 0000000..ab1d3d2
--- /dev/null
+++ b/man/showNotification.Rd
@@ -0,0 +1,91 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/notifications.R
+\name{showNotification}
+\alias{removeNotification}
+\alias{showNotification}
+\title{Show or remove a notification}
+\usage{
+showNotification(ui, action = NULL, duration = 5, closeButton = TRUE,
+  id = NULL, type = c("default", "message", "warning", "error"),
+  session = getDefaultReactiveDomain())
+
+removeNotification(id = NULL, session = getDefaultReactiveDomain())
+}
+\arguments{
+\item{ui}{Content of message.}
+
+\item{action}{Message content that represents an action. For example, this
+could be a link that the user can click on. This is separate from \code{ui}
+so customized layouts can handle the main notification content separately
+from action content.}
+
+\item{duration}{Number of seconds to display the message before it
+disappears. Use \code{NULL} to make the message not automatically
+disappear.}
+
+\item{closeButton}{If \code{TRUE}, display a button which will make the
+notification disappear when clicked. If \code{FALSE} do not display.}
+
+\item{id}{An ID string. This can be used to change the contents of an
+existing message with \code{showNotification}, or to remove it with
+\code{removeNotification}. If not provided, one will be generated
+automatically. If an ID is provided and there does not currently exist a
+notification with that ID, a new notification will be created with that ID.}
+
+\item{type}{A string which controls the color of the notification. One of
+"default" (gray), "message" (blue), "warning" (yellow), or "error" (red).}
+
+\item{session}{Session object to send notification to.}
+}
+\value{
+An ID for the notification.
+}
+\description{
+These functions show and remove notifications in a Shiny application.
+}
+\examples{
+## Only run examples in interactive R sessions
+if (interactive()) {
+# Show a message when button is clicked
+shinyApp(
+  ui = fluidPage(
+    actionButton("show", "Show")
+  ),
+  server = function(input, output) {
+    observeEvent(input$show, {
+      showNotification("Message text",
+        action = a(href = "javascript:location.reload();", "Reload page")
+      )
+    })
+  }
+)
+
+# App with show and remove buttons
+shinyApp(
+  ui = fluidPage(
+    actionButton("show", "Show"),
+    actionButton("remove", "Remove")
+  ),
+  server = function(input, output) {
+    # A queue of notification IDs
+    ids <- character(0)
+    # A counter
+    n <- 0
+
+    observeEvent(input$show, {
+      # Save the ID for removal later
+      id <- showNotification(paste("Message", n), duration = NULL)
+      ids <<- c(ids, id)
+      n <<- n + 1
+    })
+
+    observeEvent(input$remove, {
+      if (length(ids) > 0)
+        removeNotification(ids[1])
+      ids <<- ids[-1]
+    })
+  }
+)
+}
+}
+
diff --git a/man/showReactLog.Rd b/man/showReactLog.Rd
index 30e493e..6f08794 100644
--- a/man/showReactLog.Rd
+++ b/man/showReactLog.Rd
@@ -4,7 +4,11 @@
 \alias{showReactLog}
 \title{Reactive Log Visualizer}
 \usage{
-showReactLog()
+showReactLog(time = TRUE)
+}
+\arguments{
+\item{time}{A boolean that specifies whether or not to display the
+time that each reactive.}
 }
 \description{
 Provides an interactive browser-based tool for visualizing reactive
diff --git a/man/sidebarLayout.Rd b/man/sidebarLayout.Rd
index 41c5016..31a3ed2 100644
--- a/man/sidebarLayout.Rd
+++ b/man/sidebarLayout.Rd
@@ -24,8 +24,11 @@ distinct background color and typically contains input controls. The main
 area occupies 2/3 of the horizontal width and typically contains outputs.
 }
 \examples{
+## Only run examples in interactive R sessions
+if (interactive()) {
+
 # Define UI
-shinyUI(fluidPage(
+ui <- fluidPage(
 
   # Application title
   titlePanel("Hello Shiny!"),
@@ -46,7 +49,17 @@ shinyUI(fluidPage(
       plotOutput("distPlot")
     )
   )
-))
+)
+
+# Server logic
+server <- function(input, output) {
+  output$distPlot <- renderPlot({
+    hist(rnorm(input$obs))
+  })
+}
 
+# Complete app with UI and server components
+shinyApp(ui, server)
+}
 }
 
diff --git a/man/sliderInput.Rd b/man/sliderInput.Rd
index 49b9b87..0bb2171 100644
--- a/man/sliderInput.Rd
+++ b/man/sliderInput.Rd
@@ -92,6 +92,28 @@ or list of tags (using \code{\link{tag}} and friends), or raw HTML (using
 \description{
 Constructs a slider widget to select a numeric value from a range.
 }
+\examples{
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  sliderInput("obs", "Number of observations:",
+    min = 0, max = 1000, value = 500
+  ),
+  plotOutput("distPlot")
+)
+
+# Server logic
+server <- function(input, output) {
+  output$distPlot <- renderPlot({
+    hist(rnorm(input$obs))
+  })
+}
+
+# Complete app with UI and server components
+shinyApp(ui, server)
+}
+}
 \seealso{
 \code{\link{updateSliderInput}}
 
@@ -101,6 +123,7 @@ Other input.elements: \code{\link{actionButton}},
   \code{\link{dateRangeInput}}, \code{\link{fileInput}},
   \code{\link{numericInput}}, \code{\link{passwordInput}},
   \code{\link{radioButtons}}, \code{\link{selectInput}},
-  \code{\link{submitButton}}, \code{\link{textInput}}
+  \code{\link{submitButton}}, \code{\link{textAreaInput}},
+  \code{\link{textInput}}
 }
 
diff --git a/man/splitLayout.Rd b/man/splitLayout.Rd
index 20b9dd0..465cbbf 100644
--- a/man/splitLayout.Rd
+++ b/man/splitLayout.Rd
@@ -23,21 +23,33 @@ Lays out elements horizontally, dividing the available horizontal space into
 equal parts (by default).
 }
 \examples{
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+# Server code used for all examples
+server <- function(input, output) {
+  output$plot1 <- renderPlot(plot(cars))
+  output$plot2 <- renderPlot(plot(pressure))
+  output$plot3 <- renderPlot(plot(AirPassengers))
+}
+
 # Equal sizing
-splitLayout(
+ui <- splitLayout(
   plotOutput("plot1"),
   plotOutput("plot2")
 )
+shinyApp(ui, server)
 
 # Custom widths
-splitLayout(cellWidths = c("25\%", "75\%"),
+ui <- splitLayout(cellWidths = c("25\%", "75\%"),
   plotOutput("plot1"),
   plotOutput("plot2")
 )
+shinyApp(ui, server)
 
 # All cells at 300 pixels wide, with cell padding
 # and a border around everything
-splitLayout(
+ui <- splitLayout(
   style = "border: 1px solid silver;",
   cellWidths = 300,
   cellArgs = list(style = "padding: 6px"),
@@ -45,5 +57,7 @@ splitLayout(
   plotOutput("plot2"),
   plotOutput("plot3")
 )
+shinyApp(ui, server)
+}
 }
 
diff --git a/man/submitButton.Rd b/man/submitButton.Rd
index b74984b..6ce82aa 100644
--- a/man/submitButton.Rd
+++ b/man/submitButton.Rd
@@ -33,6 +33,7 @@ Other input.elements: \code{\link{actionButton}},
   \code{\link{dateRangeInput}}, \code{\link{fileInput}},
   \code{\link{numericInput}}, \code{\link{passwordInput}},
   \code{\link{radioButtons}}, \code{\link{selectInput}},
-  \code{\link{sliderInput}}, \code{\link{textInput}}
+  \code{\link{sliderInput}}, \code{\link{textAreaInput}},
+  \code{\link{textInput}}
 }
 
diff --git a/man/tabsetPanel.Rd b/man/tabsetPanel.Rd
index 063fa77..00ae537 100644
--- a/man/tabsetPanel.Rd
+++ b/man/tabsetPanel.Rd
@@ -5,7 +5,7 @@
 \title{Create a tabset panel}
 \usage{
 tabsetPanel(..., id = NULL, selected = NULL, type = c("tabs", "pills"),
-  position = c("above", "below", "left", "right"))
+  position = NULL)
 }
 \arguments{
 \item{...}{\code{\link{tabPanel}} elements to include in the tabset}
@@ -22,10 +22,8 @@ tab will be selected.}
 \item{type}{Use "tabs" for the standard look; Use "pills" for a more plain
 look where tabs are selected using a background fill color.}
 
-\item{position}{The position of the tabs relative to the content. Valid
-values are "above", "below", "left", and "right" (defaults to "above").
-Note that the \code{position} argument is not valid when \code{type} is
-"pill".}
+\item{position}{This argument is deprecated; it has been discontinued in
+Bootstrap 3.}
 }
 \value{
 A tabset that can be passed to \code{\link{mainPanel}}
diff --git a/man/textAreaInput.Rd b/man/textAreaInput.Rd
new file mode 100644
index 0000000..7665827
--- /dev/null
+++ b/man/textAreaInput.Rd
@@ -0,0 +1,73 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/input-textarea.R
+\name{textAreaInput}
+\alias{textAreaInput}
+\title{Create a textarea input control}
+\usage{
+textAreaInput(inputId, label, value = "", width = NULL, height = NULL,
+  cols = NULL, rows = NULL, placeholder = NULL, resize = NULL)
+}
+\arguments{
+\item{inputId}{The \code{input} slot that will be used to access the value.}
+
+\item{label}{Display label for the control, or \code{NULL} for no label.}
+
+\item{value}{Initial value.}
+
+\item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
+see \code{\link{validateCssUnit}}.}
+
+\item{height}{The height of the input, e.g. \code{'400px'}, or
+\code{'100\%'}; see \code{\link{validateCssUnit}}.}
+
+\item{cols}{Value of the visible character columns of the input, e.g.
+\code{80}. If used with \code{width}, \code{width} will take precedence in
+the browser's rendering.}
+
+\item{rows}{The value of the visible character rows of the input, e.g.
+\code{6}. If used with \code{height}, \code{height} will take precedence in
+the browser's rendering.}
+
+\item{placeholder}{A character string giving the user a hint as to what can
+be entered into the control. Internet Explorer 8 and 9 do not support this
+option.}
+
+\item{resize}{Which directions the textarea box can be resized. Can be one of
+\code{"both"}, \code{"none"}, \code{"vertical"}, and \code{"horizontal"}.
+The default, \code{NULL}, will use the client browser's default setting for
+resizing textareas.}
+}
+\value{
+A textarea input control that can be added to a UI definition.
+}
+\description{
+Create a textarea input control for entry of unstructured text values.
+}
+\examples{
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  textAreaInput("caption", "Caption", "Data Summary", width = "1000px"),
+  verbatimTextOutput("value")
+)
+server <- function(input, output) {
+  output$value <- renderText({ input$caption })
+}
+shinyApp(ui, server)
+
+}
+}
+\seealso{
+\code{\link{updateTextAreaInput}}
+
+Other input.elements: \code{\link{actionButton}},
+  \code{\link{checkboxGroupInput}},
+  \code{\link{checkboxInput}}, \code{\link{dateInput}},
+  \code{\link{dateRangeInput}}, \code{\link{fileInput}},
+  \code{\link{numericInput}}, \code{\link{passwordInput}},
+  \code{\link{radioButtons}}, \code{\link{selectInput}},
+  \code{\link{sliderInput}}, \code{\link{submitButton}},
+  \code{\link{textInput}}
+}
+
diff --git a/man/textInput.Rd b/man/textInput.Rd
index 78698ae..651d2dd 100644
--- a/man/textInput.Rd
+++ b/man/textInput.Rd
@@ -27,7 +27,18 @@ A text input control that can be added to a UI definition.
 Create an input control for entry of unstructured text values
 }
 \examples{
-textInput("caption", "Caption:", "Data Summary")
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  textInput("caption", "Caption", "Data Summary"),
+  verbatimTextOutput("value")
+)
+server <- function(input, output) {
+  output$value <- renderText({ input$caption })
+}
+shinyApp(ui, server)
+}
 }
 \seealso{
 \code{\link{updateTextInput}}
@@ -38,6 +49,7 @@ Other input.elements: \code{\link{actionButton}},
   \code{\link{dateRangeInput}}, \code{\link{fileInput}},
   \code{\link{numericInput}}, \code{\link{passwordInput}},
   \code{\link{radioButtons}}, \code{\link{selectInput}},
-  \code{\link{sliderInput}}, \code{\link{submitButton}}
+  \code{\link{sliderInput}}, \code{\link{submitButton}},
+  \code{\link{textAreaInput}}
 }
 
diff --git a/man/titlePanel.Rd b/man/titlePanel.Rd
index b632a84..bcfe82c 100644
--- a/man/titlePanel.Rd
+++ b/man/titlePanel.Rd
@@ -20,7 +20,13 @@ Calling this function has the side effect of including a
   explicitly using the `title` parameter of the top-level page function.
 }
 \examples{
-titlePanel("Hello Shiny!")
+## Only run examples in interactive R sessions
+if (interactive()) {
 
+ui <- fluidPage(
+  titlePanel("Hello Shiny!")
+)
+shinyApp(ui, server = function(input, output) { })
+}
 }
 
diff --git a/man/updateActionButton.Rd b/man/updateActionButton.Rd
new file mode 100644
index 0000000..cf3bcef
--- /dev/null
+++ b/man/updateActionButton.Rd
@@ -0,0 +1,81 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/update-input.R
+\name{updateActionButton}
+\alias{updateActionButton}
+\title{Change the label or icon of an action button on the client}
+\usage{
+updateActionButton(session, inputId, label = NULL, icon = NULL)
+}
+\arguments{
+\item{session}{The \code{session} object passed to function given to
+\code{shinyServer}.}
+
+\item{inputId}{The id of the input object.}
+
+\item{label}{The label to set for the input object.}
+
+\item{icon}{The icon to set for the input object. To remove the
+current icon, use \code{icon=character(0)}.}
+}
+\description{
+Change the label or icon of an action button on the client
+}
+\details{
+The input updater functions send a message to the client, telling it to
+change the settings of an input object. The messages are collected and sent
+after all the observers (including outputs) have finished running.
+
+The syntax of these functions is similar to the functions that created the
+inputs in the first place. For example, \code{\link{numericInput}()} and
+\code{updateNumericInput()} take a similar set of arguments.
+
+Any arguments with NULL values will be ignored; they will not result in any
+changes to the input object on the client.
+
+For \code{\link{radioButtons}()}, \code{\link{checkboxGroupInput}()} and
+\code{\link{selectInput}()}, the set of choices can be cleared by using
+\code{choices=character(0)}. Similarly, for these inputs, the selected item
+can be cleared by using \code{selected=character(0)}.
+}
+\examples{
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  actionButton("update", "Update other buttons"),
+  br(),
+  actionButton("goButton", "Go"),
+  br(),
+  actionButton("goButton2", "Go 2", icon = icon("area-chart")),
+  br(),
+  actionButton("goButton3", "Go 3")
+)
+
+server <- function(input, output, session) {
+  observe({
+    req(input$update)
+
+    # Updates goButton's label and icon
+    updateActionButton(session, "goButton",
+      label = "New label",
+      icon = icon("calendar"))
+
+    # Leaves goButton2's label unchaged and
+    # removes its icon
+    updateActionButton(session, "goButton2",
+      icon = character(0))
+
+    # Leaves goButton3's icon, if it exists,
+    # unchaged and changes its label
+    updateActionButton(session, "goButton3",
+      label = "New label 3")
+  })
+}
+
+shinyApp(ui, server)
+}
+}
+\seealso{
+\code{\link{actionButton}}
+}
+
diff --git a/man/updateCheckboxGroupInput.Rd b/man/updateCheckboxGroupInput.Rd
index 32e0651..b2595d1 100644
--- a/man/updateCheckboxGroupInput.Rd
+++ b/man/updateCheckboxGroupInput.Rd
@@ -39,34 +39,39 @@ changes to the input object on the client.
 
 For \code{\link{radioButtons}()}, \code{\link{checkboxGroupInput}()} and
 \code{\link{selectInput}()}, the set of choices can be cleared by using
-\code{choices=character(0)}
+\code{choices=character(0)}. Similarly, for these inputs, the selected item
+can be cleared by using \code{selected=character(0)}.
 }
 \examples{
-\dontrun{
-shinyServer(function(input, output, session) {
+## Only run examples in interactive R sessions
+if (interactive()) {
 
-  observe({
-    # We'll use the input$controller variable multiple times, so save it as x
-    # for convenience.
-    x <- input$controller
+ui <- fluidPage(
+  p("The first checkbox group controls the second"),
+  checkboxGroupInput("inCheckboxGroup", "Input checkbox",
+    c("Item A", "Item B", "Item C")),
+  checkboxGroupInput("inCheckboxGroup2", "Input checkbox 2",
+    c("Item A", "Item B", "Item C"))
+)
 
-    # Create a list of new options, where the name of the items is something
-    # like 'option label x 1', and the values are 'option-x-1'.
-    cb_options <- list()
-    cb_options[[sprintf("option label \%d 1", x)]] <- sprintf("option-\%d-1", x)
-    cb_options[[sprintf("option label \%d 2", x)]] <- sprintf("option-\%d-2", x)
+server <- function(input, output, session) {
+  observe({
+    x <- input$inCheckboxGroup
 
-    # Change values for input$inCheckboxGroup
-    updateCheckboxGroupInput(session, "inCheckboxGroup", choices = cb_options)
+    # Can use character(0) to remove all choices
+    if (is.null(x))
+      x <- character(0)
 
     # Can also set the label and select items
     updateCheckboxGroupInput(session, "inCheckboxGroup2",
-      label = paste("checkboxgroup label", x),
-      choices = cb_options,
-      selected = sprintf("option-\%d-2", x)
+      label = paste("Checkboxgroup label", length(x)),
+      choices = x,
+      selected = x
     )
   })
-})
+}
+
+shinyApp(ui, server)
 }
 }
 \seealso{
diff --git a/man/updateCheckboxInput.Rd b/man/updateCheckboxInput.Rd
index 2544fe8..6b5a222 100644
--- a/man/updateCheckboxInput.Rd
+++ b/man/updateCheckboxInput.Rd
@@ -33,19 +33,28 @@ changes to the input object on the client.
 
 For \code{\link{radioButtons}()}, \code{\link{checkboxGroupInput}()} and
 \code{\link{selectInput}()}, the set of choices can be cleared by using
-\code{choices=character(0)}
+\code{choices=character(0)}. Similarly, for these inputs, the selected item
+can be cleared by using \code{selected=character(0)}.
 }
 \examples{
-\dontrun{
-shinyServer(function(input, output, session) {
+## Only run examples in interactive R sessions
+if (interactive()) {
 
+ui <- fluidPage(
+  sliderInput("controller", "Controller", 0, 1, 0, step = 1),
+  checkboxInput("inCheckbox", "Input checkbox")
+)
+
+server <- function(input, output, session) {
   observe({
-    # TRUE if input$controller is even, FALSE otherwise.
-    x_even <- input$controller \%\% 2 == 0
+    # TRUE if input$controller is odd, FALSE if even.
+    x_even <- input$controller \%\% 2 == 1
 
     updateCheckboxInput(session, "inCheckbox", value = x_even)
   })
-})
+}
+
+shinyApp(ui, server)
 }
 }
 \seealso{
diff --git a/man/updateDateInput.Rd b/man/updateDateInput.Rd
index 7cbea1d..19ac170 100644
--- a/man/updateDateInput.Rd
+++ b/man/updateDateInput.Rd
@@ -16,7 +16,7 @@ updateDateInput(session, inputId, label = NULL, value = NULL, min = NULL,
 \item{label}{The label to set for the input object.}
 
 \item{value}{The desired date value. Either a Date object, or a string in
-\code{yyyy-mm-dd} format.}
+\code{yyyy-mm-dd} format. Supply \code{NA} to clear the date.}
 
 \item{min}{The minimum allowed date. Either a Date object, or a string in
 \code{yyyy-mm-dd} format.}
@@ -41,25 +41,31 @@ changes to the input object on the client.
 
 For \code{\link{radioButtons}()}, \code{\link{checkboxGroupInput}()} and
 \code{\link{selectInput}()}, the set of choices can be cleared by using
-\code{choices=character(0)}
+\code{choices=character(0)}. Similarly, for these inputs, the selected item
+can be cleared by using \code{selected=character(0)}.
 }
 \examples{
-\dontrun{
-shinyServer(function(input, output, session) {
+## Only run examples in interactive R sessions
+if (interactive()) {
 
-  observe({
-    # We'll use the input$controller variable multiple times, so save it as x
-    # for convenience.
-    x <- input$controller
+ui <- fluidPage(
+  sliderInput("n", "Day of month", 1, 30, 10),
+  dateInput("inDate", "Input date")
+)
 
+server <- function(input, output, session) {
+  observe({
+    date <- as.Date(paste0("2013-04-", input$n))
     updateDateInput(session, "inDate",
-      label = paste("Date label", x),
-      value = paste("2013-04-", x, sep=""),
-      min   = paste("2013-04-", x-1, sep=""),
-      max   = paste("2013-04-", x+1, sep="")
+      label = paste("Date label", input$n),
+      value = date,
+      min   = date - 3,
+      max   = date + 3
     )
   })
-})
+}
+
+shinyApp(ui, server)
 }
 }
 \seealso{
diff --git a/man/updateDateRangeInput.Rd b/man/updateDateRangeInput.Rd
index ec6e328..6a922fc 100644
--- a/man/updateDateRangeInput.Rd
+++ b/man/updateDateRangeInput.Rd
@@ -16,10 +16,10 @@ updateDateRangeInput(session, inputId, label = NULL, start = NULL,
 \item{label}{The label to set for the input object.}
 
 \item{start}{The start date. Either a Date object, or a string in
-\code{yyyy-mm-dd} format.}
+\code{yyyy-mm-dd} format. Supplying \code{NA} clears the start date.}
 
 \item{end}{The end date. Either a Date object, or a string in
-\code{yyyy-mm-dd} format.}
+\code{yyyy-mm-dd} format. Supplying \code{NA} clears the end date.}
 
 \item{min}{The minimum allowed date. Either a Date object, or a string in
 \code{yyyy-mm-dd} format.}
@@ -44,23 +44,33 @@ changes to the input object on the client.
 
 For \code{\link{radioButtons}()}, \code{\link{checkboxGroupInput}()} and
 \code{\link{selectInput}()}, the set of choices can be cleared by using
-\code{choices=character(0)}
+\code{choices=character(0)}. Similarly, for these inputs, the selected item
+can be cleared by using \code{selected=character(0)}.
 }
 \examples{
-\dontrun{
-shinyServer(function(input, output, session) {
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  sliderInput("n", "Day of month", 1, 30, 10),
+  dateRangeInput("inDateRange", "Input date range")
+)
 
+server <- function(input, output, session) {
   observe({
-    # We'll use the input$controller variable multiple times, so save it as x
-    # for convenience.
-    x <- input$controller
+    date <- as.Date(paste0("2013-04-", input$n))
 
     updateDateRangeInput(session, "inDateRange",
-      label = paste("Date range label", x),
-      start = paste("2013-01-", x, sep=""))
-      end = paste("2013-12-", x, sep=""))
+      label = paste("Date range label", input$n),
+      start = date - 1,
+      end = date + 1,
+      min = date - 5,
+      max = date + 5
+    )
   })
-})
+}
+
+shinyApp(ui, server)
 }
 }
 \seealso{
diff --git a/man/updateNumericInput.Rd b/man/updateNumericInput.Rd
index 7f9afde..7e12209 100644
--- a/man/updateNumericInput.Rd
+++ b/man/updateNumericInput.Rd
@@ -40,13 +40,22 @@ changes to the input object on the client.
 
 For \code{\link{radioButtons}()}, \code{\link{checkboxGroupInput}()} and
 \code{\link{selectInput}()}, the set of choices can be cleared by using
-\code{choices=character(0)}
+\code{choices=character(0)}. Similarly, for these inputs, the selected item
+can be cleared by using \code{selected=character(0)}.
 }
 \examples{
-\dontrun{
-shinyServer(function(input, output, session) {
+## Only run examples in interactive R sessions
+if (interactive()) {
 
-  observe({
+ui <- fluidPage(
+  sliderInput("controller", "Controller", 0, 20, 10),
+  numericInput("inNumber", "Input number", 0),
+  numericInput("inNumber2", "Input number 2", 0)
+)
+
+server <- function(input, output, session) {
+
+  observeEvent(input$controller, {
     # We'll use the input$controller variable multiple times, so save it as x
     # for convenience.
     x <- input$controller
@@ -57,7 +66,9 @@ shinyServer(function(input, output, session) {
       label = paste("Number label ", x),
       value = x, min = x-10, max = x+10, step = 5)
   })
-})
+}
+
+shinyApp(ui, server)
 }
 }
 \seealso{
diff --git a/man/updateQueryString.Rd b/man/updateQueryString.Rd
new file mode 100644
index 0000000..418012f
--- /dev/null
+++ b/man/updateQueryString.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/bookmark-state.R
+\name{updateQueryString}
+\alias{updateQueryString}
+\title{Update URL in browser's location bar}
+\usage{
+updateQueryString(queryString, session = getDefaultReactiveDomain())
+}
+\arguments{
+\item{queryString}{The new query string to show in the location bar.}
+
+\item{session}{A Shiny session object.}
+}
+\description{
+This function updates the client browser's query string in the location bar.
+It typically is called from an observer. Note that this will not work in
+Internet Explorer 9 and below.
+}
+\seealso{
+\code{\link{enableBookmarking}} for examples.
+}
+
diff --git a/man/updateRadioButtons.Rd b/man/updateRadioButtons.Rd
index 1506374..c297c7b 100644
--- a/man/updateRadioButtons.Rd
+++ b/man/updateRadioButtons.Rd
@@ -40,32 +40,35 @@ changes to the input object on the client.
 
 For \code{\link{radioButtons}()}, \code{\link{checkboxGroupInput}()} and
 \code{\link{selectInput}()}, the set of choices can be cleared by using
-\code{choices=character(0)}
+\code{choices=character(0)}. Similarly, for these inputs, the selected item
+can be cleared by using \code{selected=character(0)}.
 }
 \examples{
-\dontrun{
-shinyServer(function(input, output, session) {
+## Only run examples in interactive R sessions
+if (interactive()) {
 
-  observe({
-    # We'll use the input$controller variable multiple times, so save it as x
-    # for convenience.
-    x <- input$controller
-
-    r_options <- list()
-    r_options[[sprintf("option label \%d 1", x)]] <- sprintf("option-\%d-1", x)
-    r_options[[sprintf("option label \%d 2", x)]] <- sprintf("option-\%d-2", x)
+ui <- fluidPage(
+  p("The first radio button group controls the second"),
+  radioButtons("inRadioButtons", "Input radio buttons",
+    c("Item A", "Item B", "Item C")),
+  radioButtons("inRadioButtons2", "Input radio buttons 2",
+    c("Item A", "Item B", "Item C"))
+)
 
-    # Change values for input$inRadio
-    updateRadioButtons(session, "inRadio", choices = r_options)
+server <- function(input, output, session) {
+  observe({
+    x <- input$inRadioButtons
 
-    # Can also set the label and select an item
-    updateRadioButtons(session, "inRadio2",
-      label = paste("Radio label", x),
-      choices = r_options,
-      selected = sprintf("option-\%d-2", x)
+    # Can also set the label and select items
+    updateRadioButtons(session, "inRadioButtons2",
+      label = paste("radioButtons label", x),
+      choices = x,
+      selected = x
     )
   })
-})
+}
+
+shinyApp(ui, server)
 }
 }
 \seealso{
diff --git a/man/updateSelectInput.Rd b/man/updateSelectInput.Rd
index 2c072f8..57b4108 100644
--- a/man/updateSelectInput.Rd
+++ b/man/updateSelectInput.Rd
@@ -53,35 +53,39 @@ changes to the input object on the client.
 
 For \code{\link{radioButtons}()}, \code{\link{checkboxGroupInput}()} and
 \code{\link{selectInput}()}, the set of choices can be cleared by using
-\code{choices=character(0)}
+\code{choices=character(0)}. Similarly, for these inputs, the selected item
+can be cleared by using \code{selected=character(0)}.
 }
 \examples{
-\dontrun{
-shinyServer(function(input, output, session) {
+## Only run examples in interactive R sessions
+if (interactive()) {
 
+ui <- fluidPage(
+  p("The checkbox group controls the select input"),
+  checkboxGroupInput("inCheckboxGroup", "Input checkbox",
+    c("Item A", "Item B", "Item C")),
+  selectInput("inSelect", "Select input",
+    c("Item A", "Item B", "Item C"))
+)
+
+server <- function(input, output, session) {
   observe({
-    # We'll use the input$controller variable multiple times, so save it as x
-    # for convenience.
-    x <- input$controller
-
-    # Create a list of new options, where the name of the items is something
-    # like 'option label x 1', and the values are 'option-x-1'.
-    s_options <- list()
-    s_options[[sprintf("option label \%d 1", x)]] <- sprintf("option-\%d-1", x)
-    s_options[[sprintf("option label \%d 2", x)]] <- sprintf("option-\%d-2", x)
-
-    # Change values for input$inSelect
-    updateSelectInput(session, "inSelect", choices = s_options)
-
-    # Can also set the label and select an item (or more than one if it's a
-    # multi-select)
-    updateSelectInput(session, "inSelect2",
-      label = paste("Select label", x),
-      choices = s_options,
-      selected = sprintf("option-\%d-2", x)
+    x <- input$inCheckboxGroup
+
+    # Can use character(0) to remove all choices
+    if (is.null(x))
+      x <- character(0)
+
+    # Can also set the label and select items
+    updateSelectInput(session, "inSelect",
+      label = paste("Select input label", length(x)),
+      choices = x,
+      selected = tail(x, 1)
     )
   })
-})
+}
+
+shinyApp(ui, server)
 }
 }
 \seealso{
diff --git a/man/updateSliderInput.Rd b/man/updateSliderInput.Rd
index a1d4f17..4ae24c2 100644
--- a/man/updateSliderInput.Rd
+++ b/man/updateSliderInput.Rd
@@ -40,7 +40,8 @@ changes to the input object on the client.
 
 For \code{\link{radioButtons}()}, \code{\link{checkboxGroupInput}()} and
 \code{\link{selectInput}()}, the set of choices can be cleared by using
-\code{choices=character(0)}
+\code{choices=character(0)}. Similarly, for these inputs, the selected item
+can be cleared by using \code{selected=character(0)}.
 }
 \examples{
 ## Only run this example in interactive R sessions
diff --git a/man/updateTabsetPanel.Rd b/man/updateTabsetPanel.Rd
index 06ab5e9..4304477 100644
--- a/man/updateTabsetPanel.Rd
+++ b/man/updateTabsetPanel.Rd
@@ -25,22 +25,31 @@ or \code{navbarPage} object.}
 Change the selected tab on the client
 }
 \examples{
-\dontrun{
-shinyServer(function(input, output, session) {
-
-  observe({
-    # TRUE if input$controller is even, FALSE otherwise.
-    x_even <- input$controller \%\% 2 == 0
-
-    # Change the selected tab.
-    # Note that the tabset container must have been created with an 'id' argument
-    if (x_even) {
-      updateTabsetPanel(session, "inTabset", selected = "panel2")
-    } else {
-      updateTabsetPanel(session, "inTabset", selected = "panel1")
-    }
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(sidebarLayout(
+  sidebarPanel(
+    sliderInput("controller", "Controller", 1, 3, 1)
+  ),
+  mainPanel(
+    tabsetPanel(id = "inTabset",
+      tabPanel(title = "Panel 1", value = "panel1", "Panel 1 content"),
+      tabPanel(title = "Panel 2", value = "panel2", "Panel 2 content"),
+      tabPanel(title = "Panel 3", value = "panel3", "Panel 3 content")
+    )
+  )
+))
+
+server <- function(input, output, session) {
+  observeEvent(input$controller, {
+    updateTabsetPanel(session, "inTabset",
+      selected = paste0("panel", input$controller)
+    )
   })
-})
+}
+
+shinyApp(ui, server)
 }
 }
 \seealso{
diff --git a/man/updateTextInput.Rd b/man/updateTextAreaInput.Rd
similarity index 64%
copy from man/updateTextInput.Rd
copy to man/updateTextAreaInput.Rd
index 123db45..a357412 100644
--- a/man/updateTextInput.Rd
+++ b/man/updateTextAreaInput.Rd
@@ -1,10 +1,10 @@
 % Generated by roxygen2: do not edit by hand
 % Please edit documentation in R/update-input.R
-\name{updateTextInput}
-\alias{updateTextInput}
-\title{Change the value of a text input on the client}
+\name{updateTextAreaInput}
+\alias{updateTextAreaInput}
+\title{Change the value of a textarea input on the client}
 \usage{
-updateTextInput(session, inputId, label = NULL, value = NULL)
+updateTextAreaInput(session, inputId, label = NULL, value = NULL)
 }
 \arguments{
 \item{session}{The \code{session} object passed to function given to
@@ -17,7 +17,7 @@ updateTextInput(session, inputId, label = NULL, value = NULL)
 \item{value}{The value to set for the input object.}
 }
 \description{
-Change the value of a text input on the client
+Change the value of a textarea input on the client
 }
 \details{
 The input updater functions send a message to the client, telling it to
@@ -33,29 +33,39 @@ changes to the input object on the client.
 
 For \code{\link{radioButtons}()}, \code{\link{checkboxGroupInput}()} and
 \code{\link{selectInput}()}, the set of choices can be cleared by using
-\code{choices=character(0)}
+\code{choices=character(0)}. Similarly, for these inputs, the selected item
+can be cleared by using \code{selected=character(0)}.
 }
 \examples{
-\dontrun{
-shinyServer(function(input, output, session) {
+## Only run examples in interactive R sessions
+if (interactive()) {
 
+ui <- fluidPage(
+  sliderInput("controller", "Controller", 0, 20, 10),
+  textAreaInput("inText", "Input textarea"),
+  textAreaInput("inText2", "Input textarea 2")
+)
+
+server <- function(input, output, session) {
   observe({
     # We'll use the input$controller variable multiple times, so save it as x
     # for convenience.
     x <- input$controller
 
     # This will change the value of input$inText, based on x
-    updateTextInput(session, "inText", value = paste("New text", x))
+    updateTextAreaInput(session, "inText", value = paste("New text", x))
 
     # Can also set the label, this time for input$inText2
-    updateTextInput(session, "inText2",
+    updateTextAreaInput(session, "inText2",
       label = paste("New label", x),
       value = paste("New text", x))
   })
-})
+}
+
+shinyApp(ui, server)
 }
 }
 \seealso{
-\code{\link{textInput}}
+\code{\link{textAreaInput}}
 }
 
diff --git a/man/updateTextInput.Rd b/man/updateTextInput.Rd
index 123db45..0aef66a 100644
--- a/man/updateTextInput.Rd
+++ b/man/updateTextInput.Rd
@@ -33,12 +33,20 @@ changes to the input object on the client.
 
 For \code{\link{radioButtons}()}, \code{\link{checkboxGroupInput}()} and
 \code{\link{selectInput}()}, the set of choices can be cleared by using
-\code{choices=character(0)}
+\code{choices=character(0)}. Similarly, for these inputs, the selected item
+can be cleared by using \code{selected=character(0)}.
 }
 \examples{
-\dontrun{
-shinyServer(function(input, output, session) {
+## Only run examples in interactive R sessions
+if (interactive()) {
 
+ui <- fluidPage(
+  sliderInput("controller", "Controller", 0, 20, 10),
+  textInput("inText", "Input text"),
+  textInput("inText2", "Input text 2")
+)
+
+server <- function(input, output, session) {
   observe({
     # We'll use the input$controller variable multiple times, so save it as x
     # for convenience.
@@ -52,7 +60,9 @@ shinyServer(function(input, output, session) {
       label = paste("New label", x),
       value = paste("New text", x))
   })
-})
+}
+
+shinyApp(ui, server)
 }
 }
 \seealso{
diff --git a/man/urlModal.Rd b/man/urlModal.Rd
new file mode 100644
index 0000000..d6ab510
--- /dev/null
+++ b/man/urlModal.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/bookmark-state.R
+\name{urlModal}
+\alias{urlModal}
+\title{Generate a modal dialog that displays a URL}
+\usage{
+urlModal(url, title = "Bookmarked application link", subtitle = NULL)
+}
+\arguments{
+\item{url}{A URL to display in the dialog box.}
+
+\item{title}{A title for the dialog box.}
+
+\item{subtitle}{Text to display underneath URL.}
+}
+\description{
+The modal dialog generated by \code{urlModal} will display the URL in a
+textarea input, and the URL text will be selected so that it can be easily
+copied. The result from \code{urlModal} should be passed to the
+\code{\link{showModal}} function to display it in the browser.
+}
+
diff --git a/man/validate.Rd b/man/validate.Rd
index 1d368da..befa483 100644
--- a/man/validate.Rd
+++ b/man/validate.Rd
@@ -78,15 +78,16 @@ not necessary for the outputs to validate \code{input$x} explicitly, as long
 as \code{a} does validate it.
 }
 \examples{
-# in ui.R
-fluidPage(
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
   checkboxGroupInput('in1', 'Check some letters', choices = head(LETTERS)),
   selectizeInput('in2', 'Select a state', choices = state.name),
   plotOutput('plot')
 )
 
-# in server.R
-function(input, output) {
+server <- function(input, output) {
   output$plot <- renderPlot({
     validate(
       need(input$in1, 'Check at least one letter!'),
@@ -95,5 +96,9 @@ function(input, output) {
     plot(1:10, main = paste(c(input$in1, input$in2), collapse = ', '))
   })
 }
+
+shinyApp(ui, server)
+
+}
 }
 
diff --git a/man/verticalLayout.Rd b/man/verticalLayout.Rd
index a89d65c..a86a066 100644
--- a/man/verticalLayout.Rd
+++ b/man/verticalLayout.Rd
@@ -17,13 +17,18 @@ Create a container that includes one or more rows of content (each element
 passed to the container will appear on it's own line in the UI)
 }
 \examples{
-shinyUI(fluidPage(
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
   verticalLayout(
     a(href="http://example.com/link1", "Link One"),
     a(href="http://example.com/link2", "Link Two"),
     a(href="http://example.com/link3", "Link Three")
   )
-))
+)
+shinyApp(ui, server = function(input, output) { })
+}
 }
 \seealso{
 \code{\link{fluidPage}}, \code{\link{flowLayout}}
diff --git a/man/withProgress.Rd b/man/withProgress.Rd
index fbc7096..86fd3ee 100644
--- a/man/withProgress.Rd
+++ b/man/withProgress.Rd
@@ -7,7 +7,8 @@
 \title{Reporting progress (functional API)}
 \usage{
 withProgress(expr, min = 0, max = 1, value = min + (max - min) * 0.1,
-  message = NULL, detail = NULL, session = getDefaultReactiveDomain(),
+  message = NULL, detail = NULL, style = getShinyOption("progress.style",
+  default = "notification"), session = getDefaultReactiveDomain(),
   env = parent.frame(), quoted = FALSE)
 
 setProgress(value = NULL, message = NULL, detail = NULL,
@@ -38,6 +39,11 @@ displayed to the user, or \code{NULL} to hide the current detail message
 (if any). The detail message will be shown with a de-emphasized appearance
 relative to \code{message}.}
 
+\item{style}{Progress display style. If \code{"notification"} (the default),
+the progress indicator will show using Shiny's notification API. If
+\code{"old"}, use the same HTML and CSS used in Shiny 0.13.2 and below
+(this is for backward-compatibility).}
+
 \item{session}{The Shiny session object, as provided by \code{shinyServer} to
 the server function. The default is to automatically find the session by
 using the current reactive domain.}
@@ -72,11 +78,24 @@ Generally, \code{withProgress}/\code{incProgress}/\code{setProgress} should
 be sufficient; the exception is if the work to be done is asynchronous (this
 is not common) or otherwise cannot be encapsulated by a single scope. In that
 case, you can use the \code{Progress} reference class.
+
+As of version 0.14, the progress indicators use Shiny's new notification API.
+If you want to use the old styling (for example, you may have used customized
+CSS), you can use \code{style="old"} each time you call
+\code{withProgress()}. If you don't want to set the style each time
+\code{withProgress} is called, you can instead call
+\code{\link{shinyOptions}(progress.style="old")} just once, inside the server
+function.
 }
 \examples{
-\dontrun{
-# server.R
-shinyServer(function(input, output) {
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+ui <- fluidPage(
+  plotOutput("plot")
+)
+
+server <- function(input, output) {
   output$plot <- renderPlot({
     withProgress(message = 'Calculation in progress',
                  detail = 'This may take a while...', value = 0, {
@@ -87,7 +106,9 @@ shinyServer(function(input, output) {
     })
     plot(cars)
   })
-})
+}
+
+shinyApp(ui, server)
 }
 }
 \seealso{
diff --git a/tests/test-all.R b/tests/test-all.R
index eabb52e..e747eaf 100644
--- a/tests/test-all.R
+++ b/tests/test-all.R
@@ -1,4 +1,4 @@
 library(testthat)
 library(shiny)
 
-test_package("shiny")
+test_check("shiny")
diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R
new file mode 100644
index 0000000..4050b1d
--- /dev/null
+++ b/tests/testthat/helper.R
@@ -0,0 +1,50 @@
+# Helper function for checking that vectors have same contents, regardless of
+# order. Can be removed once something similar is incorporated into testthat
+# package. See
+# https://github.com/hadley/testthat/issues/473
+contents_identical <- function(a, b) {
+  # Convert to named vectors - needed for sorting later.
+  if (is.null(names(a))) {
+    names(a) <- rep("", length(a))
+  }
+  if (is.null(names(b))) {
+    names(b) <- rep("", length(b))
+  }
+
+  # Fast path for atomic vectors
+  if (is.atomic(a) && is.atomic(b)) {
+    # Sort first by names, then contents. This is so that the comparison can
+    # handle duplicated names.
+    a <- a[order(names(a), a)]
+    b <- b[order(names(b), b)]
+
+    return(identical(a, b))
+  }
+
+  # If we get here, we're on the slower path for lists
+
+  # Check if names are the same. If there are duplicated names, make sure
+  # there's the same number of duplicates of each.
+  if (!identical(sort(names(a)), sort(names(b)))) {
+    return(FALSE)
+  }
+
+  # Group each vector by names
+  by_names_a <- tapply(a, names(a), function(x) x)
+  by_names_b <- tapply(b, names(b), function(x) x)
+
+  # Compare each group
+  for (i in seq_along(by_names_a)) {
+    subset_a <- by_names_a[[i]]
+    subset_b <- by_names_b[[i]]
+
+    unique_subset_a <- unique(subset_a)
+    idx_a <- sort(match(subset_a, unique_subset_a))
+    idx_b <- sort(match(subset_b, unique_subset_a))
+    if (!identical(idx_a, idx_b)) {
+      return(FALSE)
+    }
+  }
+
+  TRUE
+}
diff --git a/tests/testthat/test-bookmarking.R b/tests/testthat/test-bookmarking.R
new file mode 100644
index 0000000..990b6f1
--- /dev/null
+++ b/tests/testthat/test-bookmarking.R
@@ -0,0 +1,54 @@
+context("bookmarking")
+
+test_that("Inputs and values in query string", {
+  # Normal format
+  vals <- RestoreContext$new("?_inputs_&a=1&b=2&_values_&x=3")$asList()
+  expect_true(contents_identical(vals$input, list(a=1L, b=2L)))
+  expect_identical(as.list(vals$values), list(x=3L))
+
+  # No leading '?', trailing '&', and values before inputs
+  vals <- RestoreContext$new("_values_&x=3&_inputs_&a=1&b=2&")$asList()
+  expect_true(contents_identical(vals$input, list(a=1L, b=2L)))
+  expect_identical(as.list(vals$values), list(x=3L))
+
+  # Just inputs, no values, and leading '&'
+  vals <- RestoreContext$new("&_inputs_&a=1&b=2")$asList()
+  expect_true(contents_identical(vals$input, list(a=1L, b=2L)))
+  expect_identical(as.list(vals$values), list())
+
+  # No inputs, just values
+  vals <- RestoreContext$new("?_values_&x=3")$asList()
+  expect_identical(vals$input, list())
+  expect_identical(as.list(vals$values), list(x=3L))
+
+  # Empty query string
+  vals <- RestoreContext$new("")$asList()
+  expect_identical(vals$input, list())
+  expect_identical(as.list(vals$values), list())
+
+  # Other items (not inputs and not values)
+  vals <- RestoreContext$new("?c=3&d=4")$asList()
+  expect_identical(vals$input, list())
+  expect_identical(as.list(vals$values), list())
+
+  # Multiple instances of _inputs_ or _values_
+  expect_warning(suppressMessages(RestoreContext$new("?_inputs_&a=1&_inputs_")))
+  expect_warning(suppressMessages(RestoreContext$new("?_inputs_&a=1&_inputs_&")))
+  expect_warning(suppressMessages(RestoreContext$new("?_inputs_&a=1&_inputs_&b=2")))
+  expect_warning(suppressMessages(RestoreContext$new("?_inputs_&a=1&_values_&b=2&_inputs_&")))
+  expect_warning(suppressMessages(RestoreContext$new("?_values_&a=1&_values_")))
+  expect_warning(suppressMessages(RestoreContext$new("?_inputs_&a=1&_values_&_values&b=2")))
+
+  # If there's an error in the conversion from query string, should have
+  # blank values.
+  expect_warning(suppressMessages(rc <- RestoreContext$new("?_inputs_&a=[x&b=1")))
+  expect_identical(rc$input$asList(), list())
+  expect_identical(as.list(rc$values), list())
+  expect_identical(rc$dir, NULL)
+
+  # Ignore query string if it's a subapp
+  rc <- RestoreContext$new("?w=&__subapp__=1")
+  expect_identical(rc$input$asList(), list())
+  expect_identical(as.list(rc$values), list())
+  expect_identical(rc$dir, NULL)
+})
diff --git a/inst/tests/test-bootstrap.r b/tests/testthat/test-bootstrap.r
similarity index 89%
rename from inst/tests/test-bootstrap.r
rename to tests/testthat/test-bootstrap.r
index 2304cae..29c9201 100644
--- a/inst/tests/test-bootstrap.r
+++ b/tests/testthat/test-bootstrap.r
@@ -56,7 +56,7 @@ test_that("Choices are correctly assigned names", {
   # Unnamed list
   expect_identical(
     choicesWithNames(list("a","b",3)),
-    list(a="a", b="b", "3"=3)
+    list(a="a", b="b", "3"="3")
   )
   # Vector, with some named, some not
   expect_identical(
@@ -66,23 +66,38 @@ test_that("Choices are correctly assigned names", {
   # List, with some named, some not
   expect_identical(
     choicesWithNames(list(A="a", "b", C=3, 4)),
-    list(A="a", "b"="b", C=3, "4"=4)
+    list(A="a", "b"="b", C="3", "4"="4")
   )
   # List, named, with a sub-vector
   expect_identical(
     choicesWithNames(list(A="a", B="b", C=c("d", "e"))),
     list(A="a", B="b", C=list(d="d", e="e"))
   )
+  # List, named, with a sub-vector with numeric elements
+  expect_identical(
+    choicesWithNames(list(A="a", B="b", C=c(1, 2))),
+    list(A="a", B="b", C=list(`1`="1", `2`="2"))
+  )
   # List, named, with sublist
   expect_identical(
     choicesWithNames(list(A="a", B="b", C=list("d", "e"))),
     list(A="a", B="b", C=list(d="d", e="e"))
   )
+  # List, named, with sublist with numeric elements
+  expect_identical(
+    choicesWithNames(list(A="a", B="b", C=list(1, 2))),
+    list(A="a", B="b", C=list(`1`="1", `2`="2"))
+  )
   # List, named, with a named sub-vector of length 1
   expect_identical(
     choicesWithNames(list(A="a", B="b", C=c(D="d"))),
     list(A="a", B="b", C=list(D="d"))
   )
+  # List, named, with a named sub-vector of length 1 with a numeric element
+  expect_identical(
+    choicesWithNames(list(A="a", B="b", C=c(D=1))),
+    list(A="a", B="b", C=list(D="1"))
+  )
   # List, some named, with sublist
   expect_identical(
     choicesWithNames(list(A="a", "b", C=list("d", E="e"))),
diff --git a/tests/testthat/test-diagnostics.R b/tests/testthat/test-diagnostics.R
new file mode 100644
index 0000000..78bf74e
--- /dev/null
+++ b/tests/testthat/test-diagnostics.R
@@ -0,0 +1,36 @@
+context("code diagnostics")
+
+test_that("Code diagnostics", {
+  suppressMessages({
+    expect_false(diagnoseCode(text = "div(,)"))
+    expect_false(diagnoseCode(text = "div(a,)"))
+    expect_false(diagnoseCode(text = "div(,a)"))
+    expect_false(diagnoseCode(text = "div(a,,b)"))
+    expect_false(diagnoseCode(text = "div(a,\n,b)"))
+    expect_false(diagnoseCode(text = "div(a,,b,)"))
+    expect_false(diagnoseCode(text = "div(a,b))"))
+    expect_false(diagnoseCode(text = "div()}"))
+    expect_false(diagnoseCode(text = "div())"))
+    expect_false(diagnoseCode(text = "div()]"))
+  })
+
+
+  # Ambiguous - these aren't valid R code, but they're outside the scope of
+  # diagnoseCode, at least for now.
+  # expect_false(diagnoseCode(text = "a,,b"))
+  # expect_false(diagnoseCode(text = "div(a, ==2 )"))
+  # expect_false(diagnoseCode(text = "div(a,!,b)"))
+  # expect_false(diagnoseCode(text = "1 2"))
+
+  # Should not error
+  expect_true(diagnoseCode(text = "div()"))
+  expect_true(diagnoseCode(text = "div(a)"))
+  expect_true(diagnoseCode(text = "div(a,b)"))
+  expect_true(diagnoseCode(text = "div(1,'b')"))
+  expect_true(diagnoseCode(text = "div(a,~b)"))
+  expect_true(diagnoseCode(text = "div([mtcars,,FALSE])"))
+  expect_true(diagnoseCode(text = "div(a, 1==2)"))
+
+  # Outside of () scope
+  expect_true(diagnoseCode(text = "1\n2"))
+})
diff --git a/inst/tests/test-gc.r b/tests/testthat/test-gc.r
similarity index 100%
rename from inst/tests/test-gc.r
rename to tests/testthat/test-gc.r
diff --git a/inst/tests/test-input-handler.R b/tests/testthat/test-input-handler.R
similarity index 100%
rename from inst/tests/test-input-handler.R
rename to tests/testthat/test-input-handler.R
diff --git a/inst/tests/test-modules.R b/tests/testthat/test-modules.R
similarity index 100%
rename from inst/tests/test-modules.R
rename to tests/testthat/test-modules.R
diff --git a/tests/testthat/test-options.R b/tests/testthat/test-options.R
new file mode 100644
index 0000000..6846fda
--- /dev/null
+++ b/tests/testthat/test-options.R
@@ -0,0 +1,53 @@
+context("options")
+
+test_that("Local options", {
+  # Basic options
+  shinyOptions(a = 1, b = 2)
+
+  expect_true(contents_identical(shinyOptions(),list(a = 1, b = 2)))
+  expect_identical(getShinyOption('a'), 1)
+  expect_identical(getShinyOption('b'), 2)
+
+  # Options that haven't been set
+  expect_identical(getShinyOption('c'), NULL)
+  expect_identical(getShinyOption('c', default = 10), 10)
+
+  withLocalOptions({
+    # No changes yet
+    expect_true(contents_identical(shinyOptions(), list(a = 1, b = 2)))
+    expect_identical(getShinyOption('a'), 1)
+    expect_identical(getShinyOption('b'), 2)
+
+    # Override an option
+    shinyOptions(a = 3)
+    expect_true(contents_identical(shinyOptions(), list(b = 2, a = 3)))
+    expect_identical(getShinyOption('a'), 3)
+    expect_identical(getShinyOption('b'), 2)
+
+    # Options that haven't been set
+    expect_identical(getShinyOption('c'), NULL)
+    expect_identical(getShinyOption('c', default = 10), 10)
+
+    # Another local option set
+    withLocalOptions({
+      # Override an option
+      shinyOptions(a = 4)
+      expect_true(contents_identical(shinyOptions(), list(b = 2, a = 4)))
+      expect_identical(getShinyOption('a'), 4)
+      expect_identical(getShinyOption('b'), 2)
+    })
+  })
+
+  # Should be back to original state
+  expect_true(contents_identical(shinyOptions(), list(a = 1, b = 2)))
+  expect_identical(getShinyOption('a'), 1)
+  expect_identical(getShinyOption('b'), 2)
+
+  # Setting options to NULL removes them entirely
+  shinyOptions(b = NULL)
+  expect_identical(shinyOptions(), list(a = 1))
+
+
+  # Finish tests; reset shinyOptions
+  shinyOptions(a = NULL)
+})
diff --git a/inst/tests/test-plot-coordmap.R b/tests/testthat/test-plot-coordmap.R
similarity index 63%
rename from inst/tests/test-plot-coordmap.R
rename to tests/testthat/test-plot-coordmap.R
index 79f7beb..212bcbf 100644
--- a/inst/tests/test-plot-coordmap.R
+++ b/tests/testthat/test-plot-coordmap.R
@@ -6,6 +6,16 @@ sortList <- function(x) {
   x[sort(names(x))]
 }
 
+# Extract the print.ggplot function from inside of renderPlot. Yuck.
+print_ggplot_expr <- Filter(function(x) {
+  is.call(x) &&
+  x[[1]] == as.name("<-") &&
+  x[[2]] == as.name("print.ggplot")
+}, body(renderPlot))[[1]]
+# This will create print.ggplot in the current environment
+eval(print_ggplot_expr)
+
+
 test_that("ggplot coordmap", {
   dat <- data.frame(xvar = c(0, 5), yvar = c(10, 20))
 
@@ -16,8 +26,8 @@ test_that("ggplot coordmap", {
   p <- ggplot(dat, aes(xvar, yvar)) + geom_point() +
     scale_x_continuous(expand = c(0, 0)) +
     scale_y_continuous(expand = c(0, 0))
-  png(tmpfile)
-  m <- getGgplotCoordmap(p, 1)
+  png(tmpfile, width = 500, height = 500)
+  m <- getGgplotCoordmap(print(p), 1, 72)
   dev.off()
 
   # Check mapping vars
@@ -27,12 +37,26 @@ test_that("ggplot coordmap", {
     sortList(m[[1]]$domain),
     sortList(list(left=0, right=5, bottom=10, top=20))
   )
+  # Check for no log bases
+  expect_equal(
+    sortList(m[[1]]$log),
+    sortList(list(x=NULL, y=NULL))
+  )
+  # panel_vars should be an empty named list
+  expect_identical(m[[1]]$panel_vars, list(a=1)[0])
+  # Sanity check for ranges. Checking exact range values isn't feasible due to
+  # variations in graphics devices, and possible changes to positioning in
+  # ggplot2.
+  expect_true(m[[1]]$range$left    >  20 && m[[1]]$range$left   <  70)
+  expect_true(m[[1]]$range$right   > 480 && m[[1]]$range$right  < 499)
+  expect_true(m[[1]]$range$bottom  > 450 && m[[1]]$range$bottom < 490)
+  expect_true(m[[1]]$range$top     > 1   && m[[1]]$range$top    <  20)
 
 
   # Scatterplot where aes() is declared in geom
   p <- ggplot(dat, aes(xvar)) + geom_point(aes(y=yvar))
   png(tmpfile)
-  m <- getGgplotCoordmap(p, 1)
+  m <- getGgplotCoordmap(print(p), 1, 72)
   dev.off()
 
   # Check mapping vars
@@ -42,7 +66,7 @@ test_that("ggplot coordmap", {
   # Plot with computed variable (histogram)
   p <- ggplot(dat, aes(xvar)) + geom_histogram(binwidth=1)
   png(tmpfile)
-  m <- getGgplotCoordmap(p, 1)
+  m <- getGgplotCoordmap(print(p), 1, 72)
   dev.off()
 
   # Check mapping vars - no value for y
@@ -63,7 +87,7 @@ test_that("ggplot coordmap with facet_wrap", {
     scale_y_continuous(expand = c(0, 0)) +
     facet_wrap(~ g, ncol = 2)
   png(tmpfile)
-  m <- getGgplotCoordmap(p, 1)
+  m <- getGgplotCoordmap(print(p), 1, 72)
   dev.off()
 
   # Should have 3 panels
@@ -112,7 +136,7 @@ test_that("ggplot coordmap with facet_grid", {
   # facet_grid horizontal
   p1 <- p + facet_grid(. ~ g)
   png(tmpfile)
-  m <- getGgplotCoordmap(p1, 1)
+  m <- getGgplotCoordmap(print(p1), 1, 72)
   dev.off()
 
   # Should have 3 panels
@@ -149,7 +173,7 @@ test_that("ggplot coordmap with facet_grid", {
   # facet_grid vertical
   p1 <- p + facet_grid(g ~ .)
   png(tmpfile)
-  m <- getGgplotCoordmap(p1, 1)
+  m <- getGgplotCoordmap(print(p1), 1, 72)
   dev.off()
 
   # Should have 3 panels
@@ -197,7 +221,7 @@ test_that("ggplot coordmap with 2D facet_grid", {
 
   p1 <- p + facet_grid(g ~ h)
   png(tmpfile)
-  m <- getGgplotCoordmap(p1, 1)
+  m <- getGgplotCoordmap(print(p1), 1, 72)
   dev.off()
 
   # Should have 4 panels
@@ -235,3 +259,105 @@ test_that("ggplot coordmap with 2D facet_grid", {
   expect_equal(m[[3]]$panel_vars, list(panelvar1 = dat$h[1], panelvar2 = dat$g[2]))
   expect_equal(m[[4]]$panel_vars, list(panelvar1 = dat$h[2], panelvar2 = dat$g[2]))
 })
+
+
+test_that("ggplot coordmap with various data types", {
+  tmpfile <- tempfile("test-shiny", fileext = ".png")
+  on.exit(rm(tmpfile))
+
+  # Factors
+  dat <- expand.grid(xvar = letters[1:3], yvar = LETTERS[1:4])
+  p <- ggplot(dat, aes(xvar, yvar)) + geom_point() +
+    scale_x_discrete(expand = c(0 ,0)) +
+    scale_y_discrete(expand = c(0, 0))
+  png(tmpfile)
+  m <- getGgplotCoordmap(print(p), 1, 72)
+  dev.off()
+
+  # Check domain
+  expect_equal(
+    sortList(m[[1]]$domain),
+    sortList(list(left=1, right=3, bottom=1, top=4))
+  )
+
+  # Dates and date-times
+  dat <- data.frame(
+    xvar = as.Date("2016-09-27") + c(0, 10),
+    yvar = as.POSIXct("2016-09-27 09:00:00", origin = "1960-01-01", tz = "GMT") + c(3600, 0)
+  )
+  p <- ggplot(dat, aes(xvar, yvar)) + geom_point() +
+    scale_x_date(expand = c(0 ,0)) +
+    scale_y_datetime(expand = c(0, 0))
+  png(tmpfile)
+  m <- getGgplotCoordmap(print(p), 1, 72)
+  dev.off()
+
+  # Check domain
+  expect_equal(
+    sortList(m[[1]]$domain),
+    sortList(list(
+      left   = as.numeric(dat$xvar[1]),
+      right  = as.numeric(dat$xvar[2]),
+      bottom = as.numeric(dat$yvar[2]),
+      top    = as.numeric(dat$yvar[1])
+    ))
+  )
+})
+
+test_that("ggplot coordmap with various scales and coords", {
+  tmpfile <- tempfile("test-shiny", fileext = ".png")
+  on.exit(rm(tmpfile))
+
+  # Reversed scales
+  dat <- data.frame(xvar = c(0, 5), yvar = c(10, 20))
+  p <- ggplot(dat, aes(xvar, yvar)) + geom_point() +
+    scale_x_continuous(expand = c(0 ,0)) +
+    scale_y_reverse(expand = c(0, 0))
+  png(tmpfile)
+  m <- getGgplotCoordmap(print(p), 1, 72)
+  dev.off()
+
+  # Check domain (y reversed)
+  expect_equal(
+    sortList(m[[1]]$domain),
+    sortList(list(left=0, right=5, bottom=20, top=10))
+  )
+
+  # coord_flip
+  p <- ggplot(dat, aes(xvar, yvar)) + geom_point() +
+    scale_x_continuous(expand = c(0 ,0)) +
+    scale_y_continuous(expand = c(0 ,0)) +
+    coord_flip()
+  png(tmpfile)
+  m <- getGgplotCoordmap(print(p), 1, 72)
+  dev.off()
+
+  # Check mapping vars
+  expect_equal(m[[1]]$mapping, list(x = "yvar", y = "xvar"))
+  # Check domain (y reversed)
+  expect_equal(
+    sortList(m[[1]]$domain),
+    sortList(list(left=10, right=20, bottom=0, top=5))
+  )
+
+  # Log scales and log coord transformations
+  dat <- data.frame(xvar = c(10^-1, 10^3), yvar = c(2^-2, 2^4))
+  p <- ggplot(dat, aes(xvar, yvar)) + geom_point() +
+    scale_x_log10(expand = c(0 ,0)) +
+    scale_y_continuous(expand = c(0, 0)) +
+    coord_trans(y = "log2")
+  png(tmpfile)
+  m <- getGgplotCoordmap(print(p), 1, 72)
+  dev.off()
+
+  # Check log bases
+  expect_equal(
+    sortList(m[[1]]$log),
+    sortList(list(x=10, y=2))
+  )
+  # Check domains
+  expect_equal(
+    sortList(m[[1]]$domain),
+    sortList(list(left=-1, right=3, bottom=-2, top=4))
+  )
+})
diff --git a/inst/tests/test-reactivity.r b/tests/testthat/test-reactivity.r
similarity index 91%
rename from inst/tests/test-reactivity.r
rename to tests/testthat/test-reactivity.r
index df92918..51c4537 100644
--- a/inst/tests/test-reactivity.r
+++ b/tests/testthat/test-reactivity.r
@@ -819,6 +819,66 @@ test_that("observers autodestroy (or not)", {
   })
 })
 
+test_that("observers are garbage collected when destroyed", {
+  domain <- createMockDomain()
+  rv <- reactiveValues(x = 1)
+
+  # Auto-destroy. GC on domain end.
+  a <- observe(rv$x, domain = domain)
+  # No auto-destroy. GC with rv.
+  b <- observe(rv$x, domain = domain, autoDestroy = FALSE)
+  # No auto-destroy and no reactive dependencies. GC immediately.
+  c <- observe({}, domain = domain)
+  c$setAutoDestroy(FALSE)
+  # Similar to b, but we'll set it to autoDestroy later.
+  d <- observe(rv$x, domain = domain, autoDestroy = FALSE)
+  # Like a, but we'll destroy it immediately.
+  e <- observe(rx$x, domain = domain)
+  e$destroy()
+
+  collected <- new.env(parent = emptyenv())
+
+  reg.finalizer(a, function(o) collected$a <- TRUE)
+  reg.finalizer(b, function(o) collected$b <- TRUE)
+  reg.finalizer(c, function(o) collected$c <- TRUE)
+  reg.finalizer(d, function(o) collected$d <- TRUE)
+  reg.finalizer(e, function(o) collected$e <- TRUE)
+
+  rm(list = c("a", "b", "c", "e")) # Not "d"
+
+  gc()
+  # Nothing can be GC'd yet, because all of the observers are
+  # pending execution (i.e. waiting for flushReact).
+  expect_equal(ls(collected), character())
+
+  flushReact()
+  # Now "c" can be garbage collected, because it ran and took
+  # no dependencies (and isn't tied to the session in any way).
+  # And "e" can also be garbage collected, it's been destroyed.
+  gc()
+  expect_equal(ls(collected), c("c", "e"))
+
+  domain$end()
+  # We can GC "a" as well; even though it references rv, it is
+  # destroyed when the session ends.
+  gc()
+  expect_equal(sort(ls(collected)), c("a", "c", "e"))
+
+  # It's OK to turn on auto-destroy even after the session was
+  # destroyed.
+  d$setAutoDestroy(TRUE)
+  # This should no-op.
+  d$setAutoDestroy(FALSE)
+  rm(d)
+  gc()
+  expect_equal(sort(ls(collected)), c("a", "c", "d", "e"))
+
+  rm(rv)
+  # Both rv and "b" can now be collected.
+  gc()
+  expect_equal(sort(ls(collected)), c("a", "b", "c", "d", "e"))
+})
+
 test_that("maskReactiveContext blocks use of reactives", {
   vals <- reactiveValues(x = 123)
 
diff --git a/inst/tests/test-stack.R b/tests/testthat/test-stack.R
similarity index 100%
rename from inst/tests/test-stack.R
rename to tests/testthat/test-stack.R
diff --git a/inst/tests/test-stacks.R b/tests/testthat/test-stacks.R
similarity index 73%
rename from inst/tests/test-stacks.R
rename to tests/testthat/test-stacks.R
index ebb9076..09627d2 100644
--- a/inst/tests/test-stacks.R
+++ b/tests/testthat/test-stacks.R
@@ -13,7 +13,15 @@ causeError <- function(full) {
     B()
   })
 
-  res <- try(captureStackTraces(isolate(renderTable({C()}, server = FALSE)())),
+  res <- try({
+      captureStackTraces({
+        isolate({
+          renderTable({
+            C()
+          }, server = FALSE)()
+        })
+      })
+    },
     silent = TRUE)
   cond <- attr(res, "condition", exact = TRUE)
 
@@ -43,37 +51,38 @@ test_that("integration tests", {
   df <- causeError(full = FALSE)
   # dumpTests(df)
 
-  expect_equal(df$num, c(31L, 30L, 29L, 18L, 17L, 16L, 15L,
+  expect_equal(df$num, c(32L, 31L, 30L, 19L, 18L, 17L, 16L, 15L,
     8L, 7L, 6L, 5L, 4L, 3L, 2L, 1L))
-  expect_equal(df$call, c("A", "B", "reactive C", "C", "renderTable",
-    "func", "renderTable({     C() }, server = FALSE)", "isolate",
-    "withCallingHandlers", "captureStackTraces", "doTryCatch",
+  expect_equal(df$call, c("A", "B", "<reactive:C>", "C", "renderTable",
+    "func", "origRenderFunc","renderTable({     C() }, server = FALSE)",
+    "isolate", "withCallingHandlers", "captureStackTraces", "doTryCatch",
     "tryCatchOne", "tryCatchList", "tryCatch", "try"))
   expect_equal(nzchar(df$loc), c(TRUE, TRUE, TRUE, FALSE, TRUE,
-    FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
-    FALSE))
+    FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, TRUE, FALSE, FALSE,
+    FALSE, FALSE))
 
   df <- causeError(full = TRUE)
   # dumpTests(df)
 
-  expect_equal(df$num, c(34L, 33L, 32L, 31L, 30L, 29L, 28L,
+  expect_equal(df$num, c(35L, 34L, 33L, 32L, 31L, 30L, 29L, 28L,
     27L, 26L, 25L, 24L, 23L, 22L, 21L, 20L, 19L, 18L, 17L, 16L,
     15L, 14L, 13L, 12L, 11L, 10L, 9L, 8L, 7L, 6L, 5L, 4L, 3L,
     2L, 1L))
   expect_equal(df$call, c("h", ".handleSimpleError", "stop",
-    "A", "B", "reactive C", "..stacktraceon..", ".func", "withVisible",
+    "A", "B", "<reactive:C>", "..stacktraceon..", ".func", "withVisible",
     "withCallingHandlers", "contextFunc", "env$runWith", "withReactiveDomain",
     "ctx$run", "self$.updateValue", "..stacktraceoff..", "C",
-    "renderTable", "func", "renderTable({     C() }, server = FALSE)",
-    "..stacktraceon..", "contextFunc", "env$runWith", "withReactiveDomain",
-    "ctx$run", "..stacktraceoff..", "isolate", "withCallingHandlers",
+    "renderTable", "func", "origRenderFunc",
+    "renderTable({     C() }, server = FALSE)", "..stacktraceon..",
+    "contextFunc", "env$runWith", "withReactiveDomain", "ctx$run",
+    "..stacktraceoff..", "isolate", "withCallingHandlers",
     "captureStackTraces", "doTryCatch", "tryCatchOne", "tryCatchList",
     "tryCatch", "try"))
   expect_equal(nzchar(df$loc), c(FALSE, FALSE, FALSE, TRUE,
     TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
-    FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE,
-    FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
-    FALSE, FALSE, FALSE))
+    FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, TRUE,
+    FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, TRUE,
+    FALSE, FALSE, FALSE, FALSE))
 })
 
 test_that("shiny.error", {
diff --git a/inst/tests/test-staticdocs.R b/tests/testthat/test-staticdocs.R
similarity index 53%
rename from inst/tests/test-staticdocs.R
rename to tests/testthat/test-staticdocs.R
index 79fba75..e735ecc 100644
--- a/inst/tests/test-staticdocs.R
+++ b/tests/testthat/test-staticdocs.R
@@ -1,23 +1,38 @@
 context("staticdocs")
 
 test_that("All man pages have an entry in staticdocs/index.r", {
-  if (!all(file.exists(c('../../inst/staticdocs', '../../man')))) {
-    # This test works only when run against a package directory
+  if (all(file.exists(c('../../inst/staticdocs', '../../man')))) {
+    # We're running tests on a source tree
+    mode <- "source"
+  } else if (all(file.exists(c('../../shiny/staticdocs', '../../shiny/html')))) {
+    # We're testing an installed package, possibly for R CMD check
+    mode <- "bundle"
+  } else {
+    cat("Unknown testing environment for test-staticdocs.R.\n", file = stderr())
     return()
   }
+
   # Known not to be indexed
   known_unindexed <- c("shiny-package", "stacktrace", "knitr_methods", "knitr_methods_htmltools")
 
-  indexed_topics <- local({
+  # Read in topics from a staticdocs/index.r file
+  get_indexed_topics <- function(index_path) {
     result <- character(0)
     sd_section <- function(dummy1, dummy2, section_topics) {
       result <<- c(result, section_topics)
     }
-    source("../../inst/staticdocs/index.r", local = TRUE)
+    source(index_path, local = TRUE)
     result
-  })
+  }
 
-  all_topics <- sub("\\.Rd", "", list.files("../../man", pattern = "*.Rd"))
+  if (mode == "source") {
+    indexed_topics <- get_indexed_topics("../../inst/staticdocs/index.r")
+    all_topics <- sub("\\.Rd", "", list.files("../../man", pattern = "*.Rd"))
+
+  } else if (mode == "bundle") {
+    indexed_topics <- get_indexed_topics("../../shiny/staticdocs/index.r")
+    all_topics <- unique(unname(readRDS("../../shiny/help/aliases.rds")))
+  }
 
   # This test ensures that every documented topic is included in
   # staticdocs/index.r, unless explicitly waived by specifying it
diff --git a/inst/tests/test-stop-app.R b/tests/testthat/test-stop-app.R
similarity index 100%
rename from inst/tests/test-stop-app.R
rename to tests/testthat/test-stop-app.R
diff --git a/inst/tests/test-text.R b/tests/testthat/test-text.R
similarity index 100%
rename from inst/tests/test-text.R
rename to tests/testthat/test-text.R
diff --git a/tests/testthat/test-timer.R b/tests/testthat/test-timer.R
new file mode 100644
index 0000000..f4aef72
--- /dev/null
+++ b/tests/testthat/test-timer.R
@@ -0,0 +1,25 @@
+context("timer")
+
+test_that("Scheduling works", {
+  ran <- FALSE
+  fun <- function() {
+    ran <<- TRUE
+  }
+
+  timerCallbacks$schedule(500, fun)
+
+  timerCallbacks$executeElapsed()
+  expect_false(ran)
+
+  Sys.sleep(0.1)
+  timerCallbacks$executeElapsed()
+  expect_false(ran)
+
+  Sys.sleep(0.5)
+  expect_true(timerCallbacks$executeElapsed())
+  expect_true(ran)
+
+  # Empty timerCallbacks should return FALSE
+  expect_false(timerCallbacks$executeElapsed())
+  expect_equal(0, nrow(timerCallbacks$takeElapsed()))
+})
diff --git a/inst/tests/test-ui.R b/tests/testthat/test-ui.R
similarity index 100%
rename from inst/tests/test-ui.R
rename to tests/testthat/test-ui.R
diff --git a/tests/testthat/test-update-input.R b/tests/testthat/test-update-input.R
new file mode 100644
index 0000000..27f422e
--- /dev/null
+++ b/tests/testthat/test-update-input.R
@@ -0,0 +1,37 @@
+context("Update input controls")
+
+test_that("Radio buttons and checkboxes work with modules", {
+  createModuleSession <- function(moduleId) {
+    session <- as.environment(list(
+      ns = NS(moduleId),
+      sendInputMessage = function(inputId, message) {
+        session$lastInputMessage = list(id = inputId, message = message)
+      }
+    ))
+    session
+  }
+
+  sessA <- createModuleSession("modA")
+
+  updateRadioButtons(sessA, "test1", label = "Label", choices = letters[1:5])
+  resultA <- sessA$lastInputMessage
+
+  expect_equal("test1", resultA$id)
+  expect_equal("Label", resultA$message$label)
+  expect_equal("a", resultA$message$value)
+  expect_true(grepl('"modA-test1"', resultA$message$options))
+  expect_false(grepl('"test1"', resultA$message$options))
+
+
+  sessB <- createModuleSession("modB")
+
+  updateCheckboxGroupInput(sessB, "test2", label = "Label", choices = LETTERS[1:5])
+  resultB <- sessB$lastInputMessage
+
+  expect_equal("test2", resultB$id)
+  expect_equal("Label", resultB$message$label)
+  expect_null(resultB$message$value)
+  expect_true(grepl('"modB-test2"', resultB$message$options))
+  expect_false(grepl('"test2"', resultB$message$options))
+
+})
diff --git a/inst/tests/test-url.R b/tests/testthat/test-url.R
similarity index 87%
rename from inst/tests/test-url.R
rename to tests/testthat/test-url.R
index 2eeaf7b..89e5eb8 100644
--- a/inst/tests/test-url.R
+++ b/tests/testthat/test-url.R
@@ -18,6 +18,8 @@ test_that("Query string parsing", {
   # Should be the same with or without leading question mark
   expect_identical(parseQueryString("?foo=1&bar=b"), parseQueryString("foo=1&bar=b"))
 
+  # Leading/trailing/consecutive ampersands are ignored
+  expect_identical(parseQueryString("?&a=1&&b=2&"), parseQueryString("?a=1&b=2"))
 
   # Nested and non-nested query strings
   expect_identical(
diff --git a/inst/tests/test-utils.R b/tests/testthat/test-utils.R
similarity index 100%
rename from inst/tests/test-utils.R
rename to tests/testthat/test-utils.R
diff --git a/vignettes/events.Rmd b/vignettes/events.Rmd
deleted file mode 100644
index 65c0342..0000000
--- a/vignettes/events.Rmd
+++ /dev/null
@@ -1,121 +0,0 @@
----
-title: "JavaScript Events in Shiny"
-author: "Yihui Xie"
-date: "`r Sys.Date()`"
-vignette: >
-  %\VignetteEngine{knitr::rmarkdown}
-  %\VignetteIndexEntry{JavaScript Events in Shiny}
-output: knitr:::html_vignette
----
-
-A number of JavaScript events have been supported in Shiny after v0.12.2. These events can be used to keep track of the app progress, or even manipulate the values of inputs/outputs. All event names have the prefix `shiny:`, e.g., `shiny:connected`. We can listen to these events using `.on()` in jQuery, e.g.,
-
-```javascript
-$(document).on('shiny:connected', function(event) {
-  alert('Connected to the server');
-});
-```
-
-When an event is triggered in Shiny, the `event` object may have some additional properties that can be used to query or modify the information in Shiny, as we will see later in this document. Some events can cancel the process in Shiny, e.g., stop the propogation of an input or output change to the server. Such events include `shiny:inputchanged`, `shiny:message`, `shiny:value`, `shiny:error`, `shiny:updateinput`. To cancel the Shiny process, you can use `event.preventDefault()`, e.g.,
-
-```javascript
-// no outputs will be updated since we canceled the output changes
-$(document).on('shiny:value', function(event) {
-  event.preventDefault();
-});
-```
-
-All events currently supported in Shiny are listed below. You can find a live example at https://gallery.shinyapps.io/107-events ([source](https://github.com/rstudio/shiny-examples/tree/master/107-events)).
-
-# Initial Connection and Disconnection
-
-The events `shiny:connected` and `shiny:disconnected` are triggered when an initial connection to server is established, and when a session is ended or the connection is lost for some reason, respectively.
-
-A property `socket` in the event object is used to store the web socket that is used to communicate between R and JavaScript. For example, you may query the state of the web socket via `event.socket.readyState`.
-
-# Server Status: Busy/Idle
-
-The event `shiny:busy` is triggered when something is happening on the server (e.g. an observer is running), and the event `shiny:idle` indicates when the server is idle. The event object does not carry any special properties related to Shiny.
-
-# Messages
-
-The event `shiny:inputchanged` is triggered when an input has a new value, e.g., when you click an action button, or type in a text input. The event object has properties `name` (the id of the input), `value` (the value of the input), and `inputType` (the type of the input, e.g. `shiny.action`). 
-
-For example, suppose you have a numeric input with id `foo`, you may double its value through this event:
-
-```javascript
-$(document).on('shiny:inputchanged', function(event) {
-  if (event.name === 'foo') {
-    event.value *= 2;
-  }
-});
-```
-
-The `shiny:message` is triggered when any messages are received from the server. The event has a property `message`, which is the message object (a JavaScript object).
-
-# Conditional Panels
-
-When conditional panels (see `?shiny::conditionalPanel`) are updated, the event `shiny:conditional` is triggered on the document.
-
-# Binding/Unbinding Inputs/Outputs
-
-All the events above are triggered on the document. There are a few events triggered on specific HTML elements, including the events in the following sections on input and output elements.
-
-When an input or output is bound to Shiny, the event `shiny:bound` is triggered. Similarly, there is a `shiny:unbound` event after an input/output is unbound. In these events, the event object has properties `binding` (the input/output binding object) and `bindingType` (may be `'input'` or `'output'` depending on the binding is for an input or output).
-
-# Output Events
-
-The `shiny:value` event is triggered when an output receives a value from the server. The event object has three properties: `name` (output id), `value` (output value), and `binding` (output binding).
-
-The `shiny:error` event is triggered when an error is propogated to an output. The event also has three properties like the `shiny:value` event: `name`, `error` (the error message), and `binding`.
-
-The `shiny:recalculating` and `shiny:recalculated` events are triggered before and after an output value is recalculated, respectively. Please note `shiny:recalculated` is triggered after the output value has been recalculated in R, but that does not imply the output value has been displayed on the page. Use `shiny:value` instead if you want to do something when the output value is rendered.
-
-The `shiny:visualchange` event is triggered when an output is resized, hidden, or shown. The event object has properties `visible` (`true` or `false`) and `binding` (the output binding).
-
-Since these events are triggered specifically on an output element, you may add the listener on the output element instead of the document, although the latter also works, e.g.
-
-```javascript
-$('#foo').on('shiny:value', function(event) {
-  // append a character string to the output value
-  event.value += ' Oh that is nice!';
-});
-
-// use event.target to obtain the output element
-$(document).on('shiny:value', function(event) {
-  // cancel the output of the element with id 'foo'
-  if (event.target.id === 'foo') {
-    event.preventDefault();
-  }
-});
-```
-
-# Input Events
-
-The `shiny:updateinput` event is triggered when an input is updated, e.g., when you call `updateTextInput()` in R to update the label or value of a text input. The event object has properties `message` (the update message sent from the server) and `binding` (the input binding).
-
-# Summary
-
-Here is a summary of the events. The ones that are cancelable can also be modified by users, e.g., you can change `event.value` in the `shiny:inputchanged` event, and the new `event.value` will be used as the input value (to be passed to R).
-
-```{r echo=FALSE}
-knitr::kable(matrix(c(
-  'shiny:connected', 'socket', 'No', 'document',
-  'shiny:disconnected', 'socket', 'No', 'document',
-  'shiny:busy', '', 'No', 'document',
-  'shiny:idle', '', 'No', 'document',
-  'shiny:inputchanged', 'name, value, inputType', 'Yes', 'document',
-  'shiny:message', 'message', 'Yes', 'document',
-  'shiny:conditional', '', 'No', 'document',
-  'shiny:bound', 'binding, bindingType', 'No', 'input/output element',
-  'shiny:unbound', 'binding, bindingType', 'No', 'input/output element',
-  'shiny:value', 'name, value, binding', 'Yes', 'output element',
-  'shiny:error', 'name, error, binding', 'Yes', 'output element',
-  'shiny:recalculating', '', 'No', 'output element',
-  'shiny:recalculated', '', 'No', 'output element',
-  'shiny:visualchange', 'visible, binding', 'No', 'output element',
-  'shiny:updateinput', 'message, binding', 'Yes', 'input element'
-), ncol = 4, byrow = TRUE,
-dimnames = list(NULL, c('Name', 'Event Properties', 'Cancelable', 'Target'))))
-```
-

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/r-cran-shiny.git



More information about the debian-med-commit mailing list