Compare commits

..

3 Commits

Author SHA1 Message Date
Hadley Wickham
3450c36ff3 Increase doc consistency across tab layouts 2021-03-09 07:54:07 -06:00
Hadley Wickham
b77fbe2b09 Better integrate tabPanel() and tabPanelBody() docs 2021-03-09 07:46:58 -06:00
Hadley Wickham
7d7b312078 Consistently cross-link tab layouts 2021-03-09 07:44:24 -06:00
675 changed files with 80911 additions and 47289 deletions

View File

@@ -21,14 +21,3 @@
^TODO-promises.md$ ^TODO-promises.md$
^manualtests$ ^manualtests$
^\.github$ ^\.github$
^\.vscode$
^\.madgerc$
^package\.json$
^tsconfig\.json$
^package-lock\.json$
^node_modules$
^coverage$
^.ignore$
^eslint\.config\.mjs$
^_dev$
^.claude$

2
.gitattributes vendored
View File

@@ -1,6 +1,4 @@
/NEWS merge=union /NEWS merge=union
/inst/www/shared/shiny.js -merge -diff /inst/www/shared/shiny.js -merge -diff
/inst/www/shared/shiny-*.js -merge -diff
/inst/www/shared/shiny*.css -merge -diff
*.min.js -merge -diff *.min.js -merge -diff
*.js.map -merge -diff *.js.map -merge -diff

View File

@@ -1,7 +1,7 @@
--- ---
name : Ask a Question name : Ask a Question
about : The issue tracker is not for questions -- please ask questions at https://forum.posit.co/tags/shiny. about : The issue tracker is not for questions -- please ask questions at https://community.rstudio.com/c/shiny.
--- ---
The issue tracker is not for questions. If you have a question, please feel free to ask it on our community site, at https://forum.posit.co/c/shiny. The issue tracker is not for questions. If you have a question, please feel free to ask it on our community site, at https://community.rstudio.com/c/shiny.

View File

@@ -1,13 +0,0 @@
#!/bin/bash -e
. ./tools/documentation/checkDocsCurrent.sh
echo "Updating package.json version to match DESCRIPTION Version"
Rscript ./tools/updatePackageJsonVersion.R
if [ -n "$(git status --porcelain package.json)" ]
then
echo "package.json has changed after running ./tools/updatePackageJsonVersion.R. Re-running 'npm run build'"
npm run build
git add ./inst package.json && git commit -m 'Sync package version (GitHub Actions)' || echo "No package version to commit"
else
echo "No package version difference detected; package.json is current."
fi

View File

@@ -1,21 +1,142 @@
# Workflow derived from https://github.com/rstudio/shiny-workflows name: R-CMD-check
#
# NOTE: This Shiny team GHA workflow is overkill for most R packages.
# For most R packages it is better to use https://github.com/r-lib/actions
on: on:
push: push:
branches: [main, rc-**] branches:
- master
pull_request: pull_request:
branches: [main] branches:
schedule: - master
- cron: "0 5 * * 1" # every monday
name: Package checks
jobs: jobs:
website:
uses: rstudio/shiny-workflows/.github/workflows/website.yaml@v1
routine:
uses: rstudio/shiny-workflows/.github/workflows/routine.yaml@v1
R-CMD-check: R-CMD-check:
uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1 runs-on: ${{ matrix.config.os }}
name: ${{ matrix.config.os }} (${{ matrix.config.r }})
strategy:
fail-fast: false
matrix:
config:
- {os: macOS-latest, r: 'devel'}
- {os: macOS-latest, r: '4.0'}
- {os: windows-latest, r: '4.0'}
- {os: ubuntu-16.04, r: '4.0', rspm: "https://packagemanager.rstudio.com/cran/__linux__/xenial/latest"}
- {os: ubuntu-16.04, r: '3.6', rspm: "https://packagemanager.rstudio.com/cran/__linux__/xenial/latest"}
- {os: ubuntu-16.04, r: '3.5', rspm: "https://packagemanager.rstudio.com/cran/__linux__/xenial/latest"}
- {os: ubuntu-16.04, r: '3.4', rspm: "https://packagemanager.rstudio.com/cran/__linux__/xenial/latest"}
- {os: ubuntu-16.04, r: '3.3', rspm: "https://packagemanager.rstudio.com/cran/__linux__/xenial/latest"}
env:
_R_CHECK_FORCE_SUGGESTS_: false
R_REMOTES_NO_ERRORS_FROM_WARNINGS: true
RSPM: ${{ matrix.config.rspm }}
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
steps:
# https://github.com/actions/checkout/issues/135
- name: Set git to use LF
if: runner.os == 'Windows'
run: |
git config --system core.autocrlf false
git config --system core.eol lf
- uses: actions/checkout@v2
- uses: r-lib/actions/setup-r@master
id: install-r
with:
r-version: ${{ matrix.config.r }}
- uses: r-lib/actions/setup-pandoc@master
- name: Install pak and query dependencies
shell: Rscript {0}
run: |
install.packages("pak", repos = "https://r-lib.github.io/p/pak/dev/")
saveRDS(pak::pkg_deps_tree("local::.", dependencies = TRUE), ".github/r-depends.rds")
- name: Cache R packages
uses: actions/cache@v2
with:
path: ${{ env.R_LIBS_USER }}
key: ${{ matrix.config.os }}-${{ steps.install-r.outputs.installed-r-version }}-1-${{ hashFiles('.github/r-depends.rds') }}
restore-keys: ${{ matrix.config.os }}-${{ steps.install-r.outputs.installed-r-version }}-1-
- name: Install system dependencies
if: runner.os == 'Linux'
shell: Rscript {0}
run: |
pak::local_system_requirements(execute = TRUE)
# xquartz and cairo are needed for Cairo package.
# harfbuzz and fribidi are needed for textshaping package.
- name: Mac systemdeps
if: runner.os == 'macOS'
run: |
brew install --cask xquartz
brew install cairo
brew install harfbuzz fribidi
# Use a shorter temp directory for pak installations, due to filename
# length issues on Windows. https://github.com/r-lib/pak/issues/252
- name: Windows temp dir
if: runner.os == 'Windows'
run: |
New-Item -Path "C:\" -Name "tmp" -ItemType Directory
echo "TMPDIR=c:\tmp" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Install dependencies
run: |
pak::local_install_dev_deps(upgrade = TRUE)
pak::pkg_install("rcmdcheck")
shell: Rscript {0}
- name: Find PhantomJS path
id: phantomjs
run: |
echo "::set-output name=path::$(Rscript -e 'cat(shinytest:::phantom_paths()[[1]])')"
- name: Cache PhantomJS
uses: actions/cache@v2
with:
path: ${{ steps.phantomjs.outputs.path }}
key: ${{ matrix.config.os }}-phantomjs
restore-keys: ${{ matrix.config.os }}-phantomjs
- name: Install PhantomJS
run: >
Rscript
-e "if (!shinytest::dependenciesInstalled()) shinytest::installDependencies()"
- name: Session info
run: |
options(width = 100)
pkgs <- installed.packages()[, "Package"]
sessioninfo::session_info(pkgs, include_base = TRUE)
shell: Rscript {0}
- name: Check
env:
_R_CHECK_CRAN_INCOMING_: false
run: rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "check")
shell: Rscript {0}
- name: Show testthat output
if: always()
run: find check -name 'testthat.Rout*' -exec cat '{}' \; || true
shell: bash
- name: Upload check results
if: failure()
uses: actions/upload-artifact@v2
with:
name: ${{ matrix.config.os }}-r${{ matrix.config.r }}-results
path: check
- name: Fix path for Windows caching
if: runner.os == 'Windows'
# This is needed because if you use the default tar at this stage,
# C:/Rtools/bin/tar.exe, it will say that it can't find gzip.exe. So
# we'll just set the path so that the original tar that would be
# found, will be found.
run: echo "C:/Program Files/Git/usr/bin" >> $GITHUB_PATH

View File

@@ -1,25 +0,0 @@
name: Update website docs given new release tag
on:
push:
branches:
- build_docs
tags:
- "v*"
jobs:
trigger-build:
runs-on: ubuntu-latest
steps:
- name: Send repository dispatch event
uses: actions/github-script@v8
with:
github-token: ${{ secrets.SHINY_DEV_CENTER_GITHUB_TOKEN }}
script: |
await github.rest.repos.createDispatchEvent({
owner: 'rstudio',
repo: 'shiny-dev-center',
event_type: 'build-r-reference',
client_payload: {}
});

153
.github/workflows/rituals.yaml vendored Normal file
View File

@@ -0,0 +1,153 @@
on:
push:
branches:
- master
- ghactions
pull_request:
branches:
- master
name: Rituals
jobs:
rituals:
name: Rituals
# if: false
runs-on: ${{ matrix.config.os }}
strategy:
fail-fast: false
matrix:
config:
- { os: ubuntu-16.04, r: '4.0', node: "14.x", rspm: "https://packagemanager.rstudio.com/all/__linux__/xenial/latest"}
env:
R_REMOTES_NO_ERRORS_FROM_WARNINGS: true
RSPM: ${{ matrix.config.rspm }}
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v1
- uses: r-lib/actions/pr-fetch@master
name: Git Pull (PR)
if: github.event_name == 'pull_request'
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: r-lib/actions/setup-r@master
id: install-r
with:
r-version: ${{ matrix.config.r }}
- uses: r-lib/actions/setup-pandoc@master
- name: Git Config
run: |
git config user.name "${GITHUB_ACTOR}"
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
- name: Install pak and query dependencies
shell: Rscript {0}
run: |
install.packages("pak", repos = "https://r-lib.github.io/p/pak/dev/")
saveRDS(pak::pkg_deps_tree("local::.", dependencies = TRUE), ".github/r-depends.rds")
- name: Cache R packages
uses: actions/cache@v2
with:
path: ${{ env.R_LIBS_USER }}
key: ${{ matrix.config.os }}-${{ steps.install-r.outputs.installed-r-version }}-1-${{ hashFiles('.github/r-depends.rds') }}
restore-keys: ${{ matrix.config.os }}-${{ steps.install-r.outputs.installed-r-version }}-1-
- name: Install system dependencies
# if: runner.os == 'Linux'
shell: Rscript {0}
run: |
pak::local_system_requirements(execute = TRUE)
- name: Install dependencies
shell: Rscript {0}
run: |
pak::local_install_dev_deps(upgrade = TRUE)
pak::pkg_install("sessioninfo")
pak::pkg_install("devtools")
- name: Session info
shell: Rscript {0}
run: |
options(width = 100)
pkgs <- installed.packages()[, "Package"]
sessioninfo::session_info(pkgs, include_base = TRUE)
- name: Url redirects
# only perform if in an RC branch (`rc-vX.Y.Z`)
if: ${{ github.event_name == 'push' && contains(github.ref, '/rc-v') }}
run: |
Rscript -e 'pak::pkg_install("r-lib/urlchecker"); urlchecker::url_update()'
# throw an error if man files were updated
if [ -n "$(git status --porcelain man)" ]
then
git status --porcelain
>&2 echo "Updated links found in files above"
>&2 echo 'Run `urlchecker::url_update()` to fix links locally'
exit 1
fi
# Add locally changed urls
git add .
git commit -m 'Update links (GitHub Actions)' || echo "No link changes to commit"
- name: Document
run: |
Rscript -e 'devtools::document()'
git add man/\* NAMESPACE
git commit -m 'Document (GitHub Actions)' || echo "No documentation changes to commit"
- name: Check documentation
run: |
./tools/documentation/checkDocsCurrent.sh
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.config.node }}
# https://github.com/actions/cache/blame/ccf96194800dbb7b7094edcd5a7cf3ec3c270f10/examples.md#L185-L200
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: yarn cache
uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ matrix.config.os }}-${{ matrix.config.node }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ matrix.config.os }}-${{ matrix.config.node }}-yarn-
- name: Build JS
run: |
cd srcts
tree src
yarn install --immutable && yarn build
git add ./src && git commit -m 'yarn lint (GitHub Actions)' || echo "No yarn lint changes to commit"
git add ../inst && git commit -m 'yarn build (GitHub Actions)' || echo "No yarn build changes to commit"
- name: Check JS build is latest
run: |
./tools/checkJSCurrent.sh
- name: Git Push (PR)
uses: r-lib/actions/pr-push@master
if: github.event_name == 'pull_request'
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Git Push (MASTER)
if: github.event_name == 'push'
run: |
git push https://${{github.actor}}:${{secrets.GITHUB_TOKEN}}@github.com/${{github.repository}}.git HEAD:${{ github.ref }} || echo "No changes to push"
# Execute after pushing, as no updated files will be produced
- name: Test TypeScript code
run: |
cd srcts
yarn test

11
.gitignore vendored
View File

@@ -9,16 +9,7 @@
shinyapps/ shinyapps/
README.html README.html
.*.Rnb.cached .*.Rnb.cached
/_dev/ tools/yarn-error.log
.sass_cache_keys
# TypeScript
/node_modules/
.cache
coverage/
madge.svg
# GHA remotes installation # GHA remotes installation
.github/r-depends.rds .github/r-depends.rds
.claude/settings.local.json

View File

@@ -1,7 +0,0 @@
{
"detectiveOptions": {
"ts": {
"skipTypeImports": true
}
}
}

22
.vscode/settings.json vendored
View File

@@ -1,22 +0,0 @@
{
"search.exclude": {
},
"prettier.prettierPath": "./node_modules/prettier",
"typescript.enablePromptUseWorkspaceTsdk": true,
"[r]": {
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
},
"[json]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
},
}

View File

@@ -1,132 +1,120 @@
Type: Package
Package: shiny Package: shiny
Type: Package
Title: Web Application Framework for R Title: Web Application Framework for R
Version: 1.11.1.9000 Version: 1.6.0.9000
Authors@R: c( Authors@R: c(
person("Winston", "Chang", , "winston@posit.co", role = "aut", person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
comment = c(ORCID = "0000-0002-1576-2126")), person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
person("Joe", "Cheng", , "joe@posit.co", role = "aut"), person("JJ", "Allaire", role = "aut", email = "jj@rstudio.com"),
person("JJ", "Allaire", , "jj@posit.co", role = "aut"), person("Carson", "Sievert", role = "aut", email = "carson@rstudio.com"),
person("Carson", "Sievert", , "carson@posit.co", role = c("aut", "cre"), person("Barret", "Schloerke", role = "aut", email = "barret@rstudio.com"),
comment = c(ORCID = "0000-0002-4958-2844")), person("Yihui", "Xie", role = "aut", email = "yihui@rstudio.com"),
person("Barret", "Schloerke", , "barret@posit.co", role = "aut", person("Jeff", "Allen", role = "aut", email = "jeff@rstudio.com"),
comment = c(ORCID = "0000-0001-9986-114X")), person("Jonathan", "McPherson", role = "aut", email = "jonathan@rstudio.com"),
person("Garrick", "Aden-Buie", , "garrick@adenbuie.com", role = "aut",
comment = c(ORCID = "0000-0002-7111-0077")),
person("Yihui", "Xie", , "yihui@posit.co", role = "aut"),
person("Jeff", "Allen", role = "aut"),
person("Jonathan", "McPherson", , "jonathan@posit.co", role = "aut"),
person("Alan", "Dipert", role = "aut"), person("Alan", "Dipert", role = "aut"),
person("Barbara", "Borges", role = "aut"), person("Barbara", "Borges", role = "aut"),
person("Posit Software, PBC", role = c("cph", "fnd"), person(family = "RStudio", role = "cph"),
comment = c(ROR = "03wc8by49")), person(family = "jQuery Foundation", role = "cph",
person(, "jQuery Foundation", role = "cph", comment = "jQuery library and jQuery UI library"),
comment = "jQuery library and jQuery UI library"), person(family = "jQuery contributors", role = c("ctb", "cph"),
person(, "jQuery contributors", role = c("ctb", "cph"), comment = "jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt"),
comment = "jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt"), person(family = "jQuery UI contributors", role = c("ctb", "cph"),
person(, "jQuery UI contributors", role = c("ctb", "cph"), comment = "jQuery UI library; authors listed in inst/www/shared/jqueryui/AUTHORS.txt"),
comment = "jQuery UI library; authors listed in inst/www/shared/jqueryui/AUTHORS.txt"),
person("Mark", "Otto", role = "ctb", person("Mark", "Otto", role = "ctb",
comment = "Bootstrap library"), comment = "Bootstrap library"),
person("Jacob", "Thornton", role = "ctb", person("Jacob", "Thornton", role = "ctb",
comment = "Bootstrap library"), comment = "Bootstrap library"),
person(, "Bootstrap contributors", role = "ctb", person(family = "Bootstrap contributors", role = "ctb",
comment = "Bootstrap library"), comment = "Bootstrap library"),
person(, "Twitter, Inc", role = "cph", person(family = "Twitter, Inc", role = "cph",
comment = "Bootstrap library"), comment = "Bootstrap library"),
person("Prem Nawaz", "Khan", role = "ctb", person("Prem Nawaz", "Khan", role = "ctb",
comment = "Bootstrap accessibility plugin"), comment = "Bootstrap accessibility plugin"),
person("Victor", "Tsaran", role = "ctb", person("Victor", "Tsaran", role = "ctb",
comment = "Bootstrap accessibility plugin"), comment = "Bootstrap accessibility plugin"),
person("Dennis", "Lembree", role = "ctb", person("Dennis", "Lembree", role = "ctb",
comment = "Bootstrap accessibility plugin"), comment = "Bootstrap accessibility plugin"),
person("Srinivasu", "Chakravarthula", role = "ctb", person("Srinivasu", "Chakravarthula", role = "ctb",
comment = "Bootstrap accessibility plugin"), comment = "Bootstrap accessibility plugin"),
person("Cathy", "O'Connor", role = "ctb", person("Cathy", "O'Connor", role = "ctb",
comment = "Bootstrap accessibility plugin"), comment = "Bootstrap accessibility plugin"),
person(, "PayPal, Inc", role = "cph", person(family = "PayPal, Inc", role = "cph",
comment = "Bootstrap accessibility plugin"), comment = "Bootstrap accessibility plugin"),
person("Stefan", "Petre", role = c("ctb", "cph"), person("Stefan", "Petre", role = c("ctb", "cph"),
comment = "Bootstrap-datepicker library"), comment = "Bootstrap-datepicker library"),
person("Andrew", "Rowls", role = c("ctb", "cph"), person("Andrew", "Rowls", role = c("ctb", "cph"),
comment = "Bootstrap-datepicker library"), comment = "Bootstrap-datepicker library"),
person("Dave", "Gandy", role = c("ctb", "cph"),
comment = "Font-Awesome font"),
person("Brian", "Reavis", role = c("ctb", "cph"), person("Brian", "Reavis", role = c("ctb", "cph"),
comment = "selectize.js library"), comment = "selectize.js library"),
person("Salmen", "Bejaoui", role = c("ctb", "cph"), person("Salmen", "Bejaoui", role = c("ctb", "cph"),
comment = "selectize-plugin-a11y library"), comment = "selectize-plugin-a11y library"),
person("Denis", "Ineshin", role = c("ctb", "cph"), person("Denis", "Ineshin", role = c("ctb", "cph"),
comment = "ion.rangeSlider library"), comment = "ion.rangeSlider library"),
person("Sami", "Samhuri", role = c("ctb", "cph"), person("Sami", "Samhuri", role = c("ctb", "cph"),
comment = "Javascript strftime library"), comment = "Javascript strftime library"),
person(, "SpryMedia Limited", role = c("ctb", "cph"), person(family = "SpryMedia Limited", role = c("ctb", "cph"),
comment = "DataTables library"), comment = "DataTables library"),
person("John", "Fraser", role = c("ctb", "cph"), person("John", "Fraser", role = c("ctb", "cph"),
comment = "showdown.js library"), comment = "showdown.js library"),
person("John", "Gruber", role = c("ctb", "cph"), person("John", "Gruber", role = c("ctb", "cph"),
comment = "showdown.js library"), comment = "showdown.js library"),
person("Ivan", "Sagalaev", role = c("ctb", "cph"), person("Ivan", "Sagalaev", role = c("ctb", "cph"),
comment = "highlight.js library"), comment = "highlight.js library"),
person("R Core Team", role = c("ctb", "cph"), person(family = "R Core Team", role = c("ctb", "cph"),
comment = "tar implementation from R") comment = "tar implementation from R")
) )
Description: Makes it incredibly easy to build interactive web Description: Makes it incredibly easy to build interactive web
applications with R. Automatic "reactive" binding between inputs and applications with R. Automatic "reactive" binding between inputs and
outputs and extensive prebuilt widgets make it possible to build outputs and extensive prebuilt widgets make it possible to build
beautiful, responsive, and powerful applications with minimal effort. beautiful, responsive, and powerful applications with minimal effort.
License: GPL-3 | file LICENSE License: GPL-3 | file LICENSE
URL: https://shiny.posit.co/, https://github.com/rstudio/shiny
BugReports: https://github.com/rstudio/shiny/issues
Depends: Depends:
methods, R (>= 3.0.2),
R (>= 3.0.2) methods
Imports: Imports:
bslib (>= 0.6.0),
cachem (>= 1.1.0),
cli,
commonmark (>= 1.7),
fastmap (>= 1.1.1),
fontawesome (>= 0.4.0),
glue (>= 1.3.2),
grDevices,
htmltools (>= 0.5.4),
httpuv (>= 1.5.2),
jsonlite (>= 0.9.16),
later (>= 1.0.0),
lifecycle (>= 0.2.0),
mime (>= 0.3),
promises (>= 1.3.2),
R6 (>= 2.0),
rlang (>= 0.4.10),
sourcetools,
tools,
utils, utils,
grDevices,
httpuv (>= 1.5.2),
mime (>= 0.3),
jsonlite (>= 0.9.16),
xtable,
htmltools (>= 0.5.0.9001),
R6 (>= 2.0),
sourcetools,
later (>= 1.0.0),
promises (>= 1.1.0),
tools,
crayon,
rlang (>= 0.4.10),
fastmap (>= 1.1.0),
withr, withr,
xtable commonmark (>= 1.7),
glue (>= 1.3.2),
bslib (>= 0.2.2.9002),
cachem,
ellipsis,
lifecycle (>= 0.2.0)
Suggests: Suggests:
Cairo (>= 1.5-5),
coro (>= 1.1.0),
datasets, datasets,
DT, Cairo (>= 1.5-5),
dygraphs, testthat (>= 3.0.0),
future,
ggplot2,
knitr (>= 1.6), knitr (>= 1.6),
magrittr,
markdown, markdown,
mirai,
ragg,
reactlog (>= 1.0.0),
rmarkdown, rmarkdown,
sass, ggplot2,
reactlog (>= 1.0.0),
magrittr,
shinytest (>= 1.4.0.9003),
yaml,
future,
dygraphs,
ragg,
showtext, showtext,
testthat (>= 3.2.1), sass
watcher, URL: https://shiny.rstudio.com/
yaml BugReports: https://github.com/rstudio/shiny/issues
Config/Needs/check: shinytest2
Config/testthat/edition: 3
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.3
Collate: Collate:
'globals.R' 'globals.R'
'app-state.R' 'app-state.R'
@@ -141,14 +129,12 @@ Collate:
'map.R' 'map.R'
'utils.R' 'utils.R'
'bootstrap.R' 'bootstrap.R'
'busy-indicators-spinners.R'
'busy-indicators.R'
'cache-utils.R' 'cache-utils.R'
'deprecated.R' 'deprecated.R'
'devmode.R' 'devmode.R'
'diagnose.R' 'diagnose.R'
'extended-task.R'
'fileupload.R' 'fileupload.R'
'font-awesome.R'
'graph.R' 'graph.R'
'reactives.R' 'reactives.R'
'reactive-domains.R' 'reactive-domains.R'
@@ -205,18 +191,16 @@ Collate:
'shinywrappers.R' 'shinywrappers.R'
'showcase.R' 'showcase.R'
'snapshot.R' 'snapshot.R'
'staticimports.R'
'tar.R' 'tar.R'
'test-export.R' 'test-export.R'
'test-server.R' 'test-server.R'
'test.R' 'test.R'
'update-input.R' 'update-input.R'
'utils-lang.R' 'utils-lang.R'
'utils-tags.R'
'version_bs_date_picker.R'
'version_ion_range_slider.R'
'version_jquery.R' 'version_jquery.R'
'version_jqueryui.R'
'version_selectize.R'
'version_strftime.R'
'viewer.R' 'viewer.R'
RoxygenNote: 7.1.1
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RdMacros: lifecycle
Config/testthat/edition: 3

444
LICENSE
View File

@@ -10,6 +10,7 @@ these components are included below):
- Bootstrap, https://github.com/twbs/bootstrap - Bootstrap, https://github.com/twbs/bootstrap
- bootstrap-accessibility-plugin, https://github.com/paypal/bootstrap-accessibility-plugin - bootstrap-accessibility-plugin, https://github.com/paypal/bootstrap-accessibility-plugin
- bootstrap-datepicker, https://github.com/eternicode/bootstrap-datepicker - bootstrap-datepicker, https://github.com/eternicode/bootstrap-datepicker
- Font Awesome, https://github.com/FortAwesome/Font-Awesome
- selectize.js, https://github.com/selectize/selectize.js - selectize.js, https://github.com/selectize/selectize.js
- selectize-plugin-a11y, https://github.com/SLMNBJ/selectize-plugin-a11y - selectize-plugin-a11y, https://github.com/SLMNBJ/selectize-plugin-a11y
- ion.rangeSlider, https://github.com/IonDen/ion.rangeSlider - ion.rangeSlider, https://github.com/IonDen/ion.rangeSlider
@@ -307,6 +308,449 @@ bootstrap-datepicker
limitations under the License. limitations under the License.
Font Awesome (CSS files are MIT licensed; fonts have SIL Open Font License 1.1, svgs have CC BY 4.0 License)
----------------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2014 Dave Gandy
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 (c) 2014, Dave Gandy http://fontawesome.com/,
with Reserved Font Name Font Awesome.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
=======================================================================
Creative Commons Attribution 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution 4.0 International Public License ("Public License"). To the
extent this Public License may be interpreted as a contract, You are
granted the Licensed Rights in consideration of Your acceptance of
these terms and conditions, and the Licensor grants You such rights in
consideration of benefits the Licensor receives from making the
Licensed Material available under these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
d. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
e. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
f. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
g. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
h. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
i. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
j. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
k. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
4. If You Share Adapted Material You produce, the Adapter's
License You apply must not prevent recipients of the Adapted
Material from complying with this Public License.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
selectize.js selectize.js
---------------------------------------------------------------------- ----------------------------------------------------------------------

View File

@@ -19,7 +19,6 @@ S3method("[[",shinyoutput)
S3method("[[<-",reactivevalues) S3method("[[<-",reactivevalues)
S3method("[[<-",shinyoutput) S3method("[[<-",shinyoutput)
S3method("names<-",reactivevalues) S3method("names<-",reactivevalues)
S3method(as.list,Map)
S3method(as.list,reactivevalues) S3method(as.list,reactivevalues)
S3method(as.shiny.appobj,character) S3method(as.shiny.appobj,character)
S3method(as.shiny.appobj,list) S3method(as.shiny.appobj,list)
@@ -44,7 +43,6 @@ S3method(bindEvent,reactiveExpr)
S3method(bindEvent,shiny.render.function) S3method(bindEvent,shiny.render.function)
S3method(format,reactiveExpr) S3method(format,reactiveExpr)
S3method(format,reactiveVal) S3method(format,reactiveVal)
S3method(length,Map)
S3method(names,reactivevalues) S3method(names,reactivevalues)
S3method(print,reactive) S3method(print,reactive)
S3method(print,reactivevalues) S3method(print,reactivevalues)
@@ -55,7 +53,6 @@ S3method(str,reactivevalues)
export("conditionStackTrace<-") export("conditionStackTrace<-")
export(..stacktraceoff..) export(..stacktraceoff..)
export(..stacktraceon..) export(..stacktraceon..)
export(ExtendedTask)
export(HTML) export(HTML)
export(MockShinySession) export(MockShinySession)
export(NS) export(NS)
@@ -78,7 +75,6 @@ export(br)
export(browserViewer) export(browserViewer)
export(brushOpts) export(brushOpts)
export(brushedPoints) export(brushedPoints)
export(busyIndicatorOptions)
export(callModule) export(callModule)
export(captureStackTraces) export(captureStackTraces)
export(checkboxGroupInput) export(checkboxGroupInput)
@@ -107,6 +103,7 @@ export(enableBookmarking)
export(eventReactive) export(eventReactive)
export(exportTestValues) export(exportTestValues)
export(exprToFunction) export(exprToFunction)
export(extractStackTrace)
export(fileInput) export(fileInput)
export(fillCol) export(fillCol)
export(fillPage) export(fillPage)
@@ -117,6 +114,7 @@ export(fixedRow)
export(flowLayout) export(flowLayout)
export(fluidPage) export(fluidPage)
export(fluidRow) export(fluidRow)
export(formatStackTrace)
export(freezeReactiveVal) export(freezeReactiveVal)
export(freezeReactiveValue) export(freezeReactiveValue)
export(getCurrentOutputInfo) export(getCurrentOutputInfo)
@@ -192,7 +190,6 @@ export(onRestore)
export(onRestored) export(onRestored)
export(onSessionEnded) export(onSessionEnded)
export(onStop) export(onStop)
export(onUnhandledError)
export(outputOptions) export(outputOptions)
export(p) export(p)
export(pageWithSidebar) export(pageWithSidebar)
@@ -210,18 +207,21 @@ export(radioButtons)
export(reactive) export(reactive)
export(reactiveConsole) export(reactiveConsole)
export(reactiveFileReader) export(reactiveFileReader)
export(reactivePlot)
export(reactivePoll) export(reactivePoll)
export(reactivePrint)
export(reactiveTable)
export(reactiveText)
export(reactiveTimer) export(reactiveTimer)
export(reactiveUI)
export(reactiveVal) export(reactiveVal)
export(reactiveValues) export(reactiveValues)
export(reactiveValuesToList) export(reactiveValuesToList)
export(reactlog) export(reactlog)
export(reactlogAddMark)
export(reactlogReset) export(reactlogReset)
export(reactlogShow) export(reactlogShow)
export(registerInputHandler) export(registerInputHandler)
export(registerThemeDependency) export(registerThemeDependency)
export(register_devmode_option)
export(removeInputHandler) export(removeInputHandler)
export(removeModal) export(removeModal)
export(removeNotification) export(removeNotification)
@@ -264,6 +264,7 @@ export(shinyUI)
export(showBookmarkUrlModal) export(showBookmarkUrlModal)
export(showModal) export(showModal)
export(showNotification) export(showNotification)
export(showReactLog)
export(showTab) export(showTab)
export(sidebarLayout) export(sidebarLayout)
export(sidebarPanel) export(sidebarPanel)
@@ -319,7 +320,6 @@ export(updateTextInput)
export(updateVarSelectInput) export(updateVarSelectInput)
export(updateVarSelectizeInput) export(updateVarSelectizeInput)
export(urlModal) export(urlModal)
export(useBusyIndicators)
export(validate) export(validate)
export(validateCssUnit) export(validateCssUnit)
export(varSelectInput) export(varSelectInput)
@@ -339,6 +339,8 @@ import(httpuv)
import(methods) import(methods)
import(mime) import(mime)
import(xtable) import(xtable)
importFrom(ellipsis,check_dots_empty)
importFrom(ellipsis,check_dots_unnamed)
importFrom(fastmap,fastmap) importFrom(fastmap,fastmap)
importFrom(fastmap,is.key_missing) importFrom(fastmap,is.key_missing)
importFrom(fastmap,key_missing) importFrom(fastmap,key_missing)
@@ -383,7 +385,6 @@ importFrom(htmltools,tags)
importFrom(htmltools,validateCssUnit) importFrom(htmltools,validateCssUnit)
importFrom(htmltools,withTags) importFrom(htmltools,withTags)
importFrom(lifecycle,deprecated) importFrom(lifecycle,deprecated)
importFrom(lifecycle,is_present)
importFrom(promises,"%...!%") importFrom(promises,"%...!%")
importFrom(promises,"%...>%") importFrom(promises,"%...>%")
importFrom(promises,as.promise) importFrom(promises,as.promise)
@@ -392,20 +393,14 @@ importFrom(promises,promise)
importFrom(promises,promise_reject) importFrom(promises,promise_reject)
importFrom(promises,promise_resolve) importFrom(promises,promise_resolve)
importFrom(rlang,"%||%") importFrom(rlang,"%||%")
importFrom(rlang,"fn_body<-")
importFrom(rlang,"fn_fmls<-")
importFrom(rlang,as_function) importFrom(rlang,as_function)
importFrom(rlang,as_quosure) importFrom(rlang,as_quosure)
importFrom(rlang,check_dots_empty)
importFrom(rlang,check_dots_unnamed)
importFrom(rlang,enexpr) importFrom(rlang,enexpr)
importFrom(rlang,enquo) importFrom(rlang,enquo)
importFrom(rlang,enquo0)
importFrom(rlang,enquos) importFrom(rlang,enquos)
importFrom(rlang,enquos0) importFrom(rlang,enquos0)
importFrom(rlang,eval_tidy) importFrom(rlang,eval_tidy)
importFrom(rlang,expr) importFrom(rlang,expr)
importFrom(rlang,fn_body)
importFrom(rlang,get_env) importFrom(rlang,get_env)
importFrom(rlang,get_expr) importFrom(rlang,get_expr)
importFrom(rlang,inject) importFrom(rlang,inject)
@@ -413,15 +408,10 @@ importFrom(rlang,is_false)
importFrom(rlang,is_missing) importFrom(rlang,is_missing)
importFrom(rlang,is_na) importFrom(rlang,is_na)
importFrom(rlang,is_quosure) importFrom(rlang,is_quosure)
importFrom(rlang,list2)
importFrom(rlang,maybe_missing) importFrom(rlang,maybe_missing)
importFrom(rlang,missing_arg) importFrom(rlang,missing_arg)
importFrom(rlang,new_function) importFrom(rlang,new_function)
importFrom(rlang,new_quosure) importFrom(rlang,new_quosure)
importFrom(rlang,pairlist2) importFrom(rlang,pairlist2)
importFrom(rlang,quo) importFrom(rlang,quo)
importFrom(rlang,quo_get_expr)
importFrom(rlang,quo_is_missing)
importFrom(rlang,quo_set_env)
importFrom(rlang,quo_set_expr)
importFrom(rlang,zap_srcref) importFrom(rlang,zap_srcref)

569
NEWS.md
View File

@@ -1,333 +1,5 @@
# shiny (development version) shiny 1.6.0.9000
================
## New features
* The `icon` argument of `updateActionButton()`/`updateActionLink()` nows allows values other than `shiny::icon()` (e.g., `fontawesome::fa()`, `bsicons::bs_icon()`, etc). (#4249)
## Bug fixes
* `updateActionButton()`/`updateActionLink()` now correctly renders HTML content passed to the `label` argument. (#4249)
* Fixed an issue where `updateSelectizeInput(options = list(plugins="remove_button"))` could lead to multiple remove buttons. (#4275)
## Changes
* The return value of `actionButton()`/`actionLink()` changed slightly: `label` and `icon` are wrapped in an additional HTML container element. This allows for: 1. `updateActionButton()`/`updateActionLink()` to distinguish between the `label` and `icon` when making updates and 2. spacing between `label` and `icon` to be more easily customized via CSS.
# shiny 1.11.1
This is a patch release primarily for addressing the bugs introduced in v1.11.0.
## Bug fixes
* Fixed an issue where `InputBinding` implementations that don't pass a value to their `subscribe` callback were no longer notifying Shiny of input changes. (#4243)
* `updateActionButton()` and `updateActionLink()` once again handle `label` updates correctly. (#4245)
# shiny 1.11.0
## Improvements
* When auto-reload is enabled, Shiny now reloads the entire app when support files, like Shiny modules, additional script files, or web assets, change. To enable auto-reload, call `devmode(TRUE)` to enable Shiny's developer mode, or set `options(shiny.autoreload = TRUE)` to specifically enable auto-reload. You can choose which files are watched for changes with the `shiny.autoreload.pattern` option. (#4184)
* When busy indicators are enabled (i.e., `useBusyIndicators()`), Shiny now:
* Shows a spinner on recalculating htmlwidgets that have previously rendered an error (including `req()` and `validate()`). (#4172)
* Shows a spinner on `tableOutput()`. (#4172)
* Places a minimum height on recalculating outputs so that the spinner is always visible. (#4172)
* Shiny now uses `{cli}` instead of `{crayon}` for rich log messages. (thanks @olivroy, #4170)
* `renderPlot()` was updated to accommodate changes in ggplot2 v4.0.0. (#4226)
* When adding the new tab via `insertTab()` or `bslib::nav_insert()`, the underlying JavaScript no longer renders content twice. (#4179)
## New features
* `textInput()`, `textAreaInput()`, `numericInput()` and `passwordInput()` all gain an `updateOn` option. `updateOn = "change"` is the default and previous behavior, where the input value updates immediately whenever the value changes. With `updateOn = "blur"`, the input value will update only when the text input loses focus or when the user presses Enter (or Cmd/Ctrl + Enter for `textAreaInput()`). (#4183)
* `textAreaInput()` gains a `autoresize` option, which automatically resizes the text area to fit its content. (#4210)
* The family of `update*Input()` functions can now render HTML content passed to the `label` argument (e.g., `updateInputText(label = tags$b("New label"))`). (#3996)
* `ExtendedTask` now catches synchronous values and errors and returns them via `$result()`. Previously, the extended task function was required to always return a promise. This change makes it easier to use `ExtendedTask` with a function that may return early or do some synchronous work before returning a promise. (#4225)
* The `callback` argument of Shiny.js' `InputBinding.subscribe()` method gains support for a value of `"event"`. This makes it possible for an input binding to use event priority when updating the value (i.e., send immediately and always resend, even if the value hasn't changed). (#4211)
## Changes
* Shiny no longer suspends input changes when _any_ `<input type="submit">` or `<button type="submit">` is on the page. Instead, it now only suspends when a `submitButton()` is present. If you have reason for creating a submit button from custom HTML, add a CSS class of `shiny-submit-button` to the button. (#4209)
* Shiny's JavaScript assets are now compiled to ES2021 instead of ES5. (#4066)
* Upgraded jQuery from 3.6.0 to 3.7.1. (#3969)
* Updated jQuery UI from 1.13.2 to 1.14.1. (#4175)
## Bug fixes
* The Shiny Client Console (enabled with `shiny::devmode()`) no longer displays duplicate warning or error message. (#4177)
* Synchronous errors that occur inside a `ExtendedTask` no longer stop the session. (#4225)
* Calling `removeModal()` immediately after `showModal()` no longer fails to remove the modal (this would sometimes happen if the remove message was received while the modal was in the process of being revealed). (#4173)
* `runExample("08_html")` now (correctly) requests to 'shiny.min.css', eliminating a network request failure. (#4220)
* `shiny::shinyAppTemplate()` no longer errors without a call to `library(shiny)`. (#3870)
# shiny 1.10.0
## New features and improvements
* When busy indicators are enabled (i.e., `useBusyIndicators()` is in the UI), Shiny now:
* Shows the pulse indicator when dynamic UI elements are recalculating and no other spinners are visible in the app. (#4137)
* Makes the pulse indicator slightly smaller by default and improves its appearance to better blend with any background. (#4122)
* Improve collection of deep stack traces (stack traces that are tracked across steps in an async promise chain) with `{coro}` async generators such as `{elmer}` chat streams. Previously, Shiny treated each iteration of an async generator as a distinct deep stack, leading to pathologically long stack traces; now, Shiny only keeps/prints unique deep stack trace, discarding duplicates. (#4156)
* Added an example to the `ExtendedTask` documentation. (@daattali #4087)
## Bug fixes
* Fixed a bug in `conditionalPanel()` that would cause the panel to repeatedly show/hide itself when the provided condition was not boolean. (@kamilzyla, #4127)
* Fixed a bug with `sliderInput()` when used as a range slider that made it impossible to change the slider value when both handles were at the maximum value. (#4131)
* `dateInput()` and `dateRangeInput()` no longer send immediate updates to the server when the user is typing a date input. Instead, it waits until the user presses Enter or clicks out of the field to send the update, avoiding spurious and incorrect date values. Note that an update is still sent immediately when the field is cleared. (#3664)
* Fixed a bug in `onBookmark()` hook that caused elements to not be excluded from URL bookmarking. (#3762)
* Fixed a bug with stack trace capturing that caused reactives with very long async promise chains (hundreds/thousands of steps) to become extremely slow. Chains this long are unlikely to be written by hand, but `{coro}` async generators and `{elmer}` async streaming were easily creating problematically long chains. (#4155)
* Duplicate input and output IDs -- e.g. using `"debug"` for two inputs or two outputs -- or shared IDs -- e.g. using `"debug"` as the `inputId` for an input and an output -- now result in a console warning message, but not an error. When `devmode()` is enabled, an informative message is shown in the Shiny Client Console. We recommend all Shiny devs enable `devmode()` when developing Shiny apps locally. (#4101)
* Updating the choices of a `selectizeInput()` via `updateSelectizeInput()` with `server = TRUE` no longer retains the selected choice as a deselected option if the current value is not part of the new choices. (@dvg-p4 #4142)
* Fixed a bug where stack traces from `observeEvent()` were being stripped of stack frames too aggressively. (#4163)
# shiny 1.9.1
## Bug fixes
* Fixed a bug introduced in v1.9.0 where the boundaries of hover/click/brush regions on plots were being incorrectly scaled when browser zoom was used. (#4111)
# shiny 1.9.0
## New busy indication feature
Add the new `useBusyIndicators()` function to any UI definition to:
1. Add a spinner overlay on calculating/recalculating outputs.
2. Show a page-level pulsing banner when Shiny is busy calculating something (e.g., a download, side-effect, etc), but no calculating/recalculating outputs are visible.
In a future version of Shiny, busy indication will be enabled by default, so we encourage you to try it out now, provide feedback, and report any issues.
In addition, various properties of the spinners and pulse can be customized with `busyIndicatorOptions()`. For more details, see `?busyIndicatorOptions`. (#4040, #4104)
## New features and improvements
* The client-side TypeScript code for Shiny has been refactored so that the `Shiny` object is now an instance of class `ShinyClass`. (#4063)
* In TypeScript, the `Shiny` object has a new property `initializedPromise`, which is a Promise-like object that can be `await`ed or chained with `.then()`. This Promise-like object corresponds to the `shiny:sessioninitialized` JavaScript event, but is easier to use because it can be used both before and after the events have occurred. (#4063)
* Output bindings now include the `.recalculating` CSS class when they are first bound, up until the first render. This makes it possible/easier to show progress indication when the output is calculating for the first time. (#4039)
* A new `shiny.client_devmode` option controls client-side devmode features, in particular the client-side error console introduced in shiny 1.8.1, independently of the R-side features of `shiny::devmode()`. This usage is primarily intended for automatic use in Shinylive. (#4073)
* Added function `reactlogAddMark()` to programmatically add _mark_ed locations in the reactlog log without the requirement of keyboard bindings during an idle reactive moment. (#4103)
## Bug fixes
* `downloadButton()` and `downloadLink()` are now disabled up until they are fully initialized. This prevents the user from clicking the button/link before the download is ready. (#4041)
* Output bindings that are removed, invalidated, then inserted again (while invalidated) now correctly include the `.recalculating` CSS class. (#4039)
* Fixed a recent issue with `uiOutput()` and `conditionalPanel()` not properly lower opacity when recalculation (in a Bootstrap 5 context). (#4027)
* Image outputs that were scaled by CSS had certain regions that were unresponsive to hover/click/brush handlers. (#3234)
# shiny 1.8.1.1
* In v1.8.1, shiny.js starting throwing an error when input/output bindings have duplicate IDs. This error is now only thrown when `shiny::devmode(TRUE)` is enabled, so the issue is still made discoverable through the JS error console, but avoids unnecessarily breaking apps that happen to work with duplicate IDs. (#4019)
# shiny 1.8.1
## New features and improvements
* Added `ExtendedTask`, a new simple way to launch long-running asynchronous tasks that are truly non-blocking. That is, even _within_ a session, an `ExtendedTask` won't block the main thread from flushing the reactive graph (i.e., UI updates won't be blocked). `ExtendedTask` pairs nicely with new `bslib::input_task_button()` and `bslib::bind_task_button()` functions, which help give user feedback and prevent extra button clicks. (#3958)
* Added a JavaScript error dialog, reporting errors that previously were only discoverable by opening the browser's devtools open. Since this dialog is mainly useful for debugging and development, it must be enabled with `shiny::devmode()`. (#3931)
* `runExample()` now uses the `{bslib}` package to generate a better looking result. It also gains a `package` argument so that other packages can leverage this same function to run Shiny app examples. For more, see `?runExample`. (#3963, #4005)
* Added `onUnhandledError()` to register a function that will be called when an unhandled error occurs in a Shiny app. Note that this handler doesn't stop the error or prevent the session from closing, but it can be used to log the error or to clean up session-specific resources. (thanks @JohnCoene, #3993)
## Changes
* `renderDataTable()`/`dataTableOutput()` are officially deprecated in favor of [their `{DT}` equivalents](https://rstudio.github.io/DT/shiny.html). Migrating to `{DT}`, in most cases, just requires changing `renderDataTable()` to `DT::renderDT()` and `dataTableOutput()` to `DT::DTOutput()`. Also, to promote migration, when a recent version of `{DT}` is available, `renderDataTable()`/`dataTableOutput()` now automatically use their `{DT}` equivalent (and provide a message that they are doing so). If this happens to degrade an existing app, set `options(shiny.legacy.datatable = TRUE)` to get the old (i.e., non-`{DT}`) implementation. (#3998)
* Both `conditionalPanel()` and `uiOutput()` are now styled with `display: contents` by default in Shiny apps that use Bootstrap 5. This means that the elements they contain are positioned as if they were direct children of the parent container holding the `conditionalPanel()` or `uiOutput()`. This is probably what most users intend when they use these functions, but it may break apps that applied styles directly to the container elements created by these two functions. In that case, you may include CSS rules to set `display: block` for the `.shiny-panel-conditional` or `.shiny-html-output` classes. (#3957, #3960)
## Bug fixes
* Notifications are now constrained to the width of the viewport for window widths smaller the default notification panel size. (#3949)
* Fixed #2392: `downloadButton()` now visibly returns its HTML tag so that it renders correctly in R Markdown and Quarto output. (Thanks to @fennovj, #2672)
* Calling `updateSelectizeInput()` with `choices` and `selected` now clears the current selection before updating the choices and selected value. (#3967)
* Loading a Shiny app in a package-like directory will no longer warn if autoloading is disabled by the presence of an `R/_disable_autoload.R` file. (Thanks to @krlmlr and @tanho63, #3513)
# shiny 1.8.0
## Breaking changes
* Closed #3899: The JS function `Shiny.bindAll()` is now asynchronous. This change is driven by the recent push toward making dynamic UI rendering asynchronous, which is necessary for [shinylive](https://shinylive.io/r) (and should've happened when it was first introduced in Shiny v1.7.5). The vast majority of existing `Shiny.bindAll()` uses should continue to work as before, but some cases may break if downstream code relies on it being synchronous (i.e., blocking the main thread). In this case, consider placing any downstream code in a `.then()` callback (or `await` the result in a `async` function). (#3929)
* Since `renderContent()` calls `bindAll()` (after it inserts content), it now returns a `Promise<void>` instead of `void`, which can be useful if downstream code needs to wait for the binding to complete.
## New features and improvements
* Updated `selectizeInput()`'s selectize.js dependency from v0.12.4 to v0.15.2. In addition to many bug fixes and improvements, this update also adds several new [plugin options](https://selectize.dev/docs/demos/plugins). (#3875)
* Shiny's CSS styling (for things like `showNotification()`, `withProgress()`, `inputPanel()`, etc.), has been updated with `{bslib}`'s upcoming CSS-only dark mode feature in mind. (#3882, #3914)
* Default styles for `showNotification()` were tweaked slightly to improve accessibility, sizing, and padding. (#3913)
* Shiny inputs and `{htmlwidgets}` are no longer treated as draggable inside of `absolutePanel()`/`fixedPanel()` with `draggable = TRUE`. As a result, interactions like zooming and panning now work as expected with widgets like `{plotly}` and `{leaflet}` when they appear in a draggable panel. (#3752, #3933)
* For `InputBinding`s, the `.receiveMessage()` method can now be asynchronous or synchronous (previously it could only be synchronous). (#3930)
## Bug fixes
* `fileInput()` no longer has unwanted round corners applied to the `buttonLabel`. (#3879)
* Fixed #3898: `wrapFunctionLabel()` no longer throws an error if the `name` is longer than 10000 bytes. (#3903)
# shiny 1.7.5.1
## Bug fixes
* On r-devel (R > 4.3.1), `isTruthy(NULL)` now returns `FALSE` (as it does with older versions of R). (#3906)
# shiny 1.7.5
## Possibly breaking changes
* For `reactiveValues()` objects, whenever the `$names()` or `$values()` methods are called, the keys are now returned in the order that they were inserted. (#3774)
* The value provided to `options(shiny.json.digits)` is now interpreted as number of _digits after the decimal_ instead of _significant digits_. To treat the value as significant digits, wrap it in `I()` (e.g., `options(shiny.json.digits = I(4))`). This new default behavior not only helps with reducing digits in testing snapshots, but is also more consistent with `{jsonlite}`'s default behavior. (#3819)
## New features and improvements
* Closed #789: Dynamic UI is now rendered asynchronously, thanks in part to the newly exported `Shiny.renderDependenciesAsync()`, `Shiny.renderHtmlAsync()`, and `Shiny.renderContentAsync()`. Importantly, this means `<script>` tags are now loaded asynchronously (the old way used `XMLHttpRequest`, which is synchronous). In addition, `Shiny` now manages a queue of async tasks (exposed via `Shiny.shinyapp.taskQueue`) so that order of execution is preserved. (#3666)
* Fixes #3840: `updateSliderInput()` now warns when attempting to set invalid `min`, `max`, or `value` values. Sending an invalid update message to an input no longer causes other update messages to fail. (#3843)
* `sliderInput()` now has a larger target area for clicking or tapping on the slider handle or range. (#3859)
* Closed #2956: Component authors can now prevent Shiny from creating an input binding on specific elements by adding the `data-shiny-no-bind-input` attribute to the element. The attribute may have any or no value; its presence will prevent binding. This feature is primarily useful for input component authors who want to use standard HTML input elements without causing Shiny to create an input binding for them. Additionally, Shiny now adds custom classes to its inputs. For example, `checkboxInput()` now has a `shiny-input-checkbox` class. These custom classes may be utilized in future updates to Shiny's input binding logic. (#3861)
* `Map` objects are now initialized at load time instead of build time. This avoids potential problems that could arise from storing `fastmap` objects into the built Shiny package. (#3775)
## Bug fixes
* Fixed #3771: Sometimes the error `ion.rangeSlider.min.js: i.stopPropagation is not a function` would appear in the JavaScript console. (#3772)
* Fixed #3833: When `width` is provided to `textAreaInput()`, we now correctly set the width of the `<textarea>` element. (#3838)
# shiny 1.7.4.1
## Full changelog
* Closed #3849: In R-devel, a warning was raised when Shiny was loaded because `as.numeric_version()` was called with a number instead of a string. (#3850)
# shiny 1.7.4
## Full changelog
### Breaking changes
* Closed #3719: Output container sizes, which are available via [`session$clientData` and `getCurrentOutputInfo()`](https://shiny.rstudio.com/articles/client-data.html), no longer round to the nearest pixel (i.e., they are now more exact, possibly fractional values). (#3720)
* Closed #3704, #3735, and #3740: `renderPlot()` no longer generates an error (or segfault) when it executes before the output is visible. Instead, it'll now use the graphics device's default size for it's initial size. Relatedly, `plotPNG()` now ignores `NULL` values for `width`/`height` (and uses the device's default `width`/`height` instead). (#3739)
### New features and improvements
* `plotOutput()`, `imageOutput()`, and `uiOutput()` gain a `fill` argument. If `TRUE` (the default for `plotOutput()`), the output container is allowed to grow/shrink to fit a fill container (created via `htmltools::bindFillRole()`) with an opinionated height. This means `plotOutput()` will grow/shrink by default [inside of `bslib::card_body_fill()`](https://rstudio.github.io/bslib/articles/cards.html#responsive-sizing), but `imageOutput()` and `uiOutput()` will have to opt-in to similar behavior with `fill = TRUE`. (#3715)
* Closed #3687: Updated jQuery-UI to v1.13.2. (#3697)
* Internal: Added clearer and strict TypeScript type definitions (#3644)
# shiny 1.7.3
### Bug fixes
* Shiny 1.7.0 changed the `icon(lib="fontawesome")` implementation from a bundled copy of fontawesome, to the {fontawesome} package. This led to issue #3688, where icons that were previously working, were now breaking. That's because {fontawesome} 0.3.0 and earlier did not have support for icon names used in Font Awesome 5 and earlier, only the newest icon names used in Font Awesome 6. Now, {fontawesome} 0.4.0 has restored support for those older icon names, and Shiny 1.7.2.1 has updated its {fontawesome} requirement to >=0.4.0.
# shiny 1.7.2
## Full changelog
### Breaking changes
* Closed #3626: `renderPlot()` (and `plotPNG()`) now uses `ragg::agg_png()` by default when the [`{ragg}` package](https://github.com/r-lib/ragg) is installed. To restore the previous behavior, set `options(shiny.useragg = FALSE)`. (#3654)
### New features and improvements
* Closed #1545: `insertUI()` now executes `<script>` tags. (#3630)
* `fileInput()` can set the `capture` attribute to facilitates user access to a device's media capture mechanism, such as a camera, or microphone, from within a file upload control ([W3C HTML Media Capture](https://www.w3.org/TR/html-media-capture/)). (Thanks to khaled-alshamaa, #3481)
* Closed tidyverse/dplyr#5552: Compatibility of dplyr 1.0 (and rlang chained errors in general) with `req()`, `validate()`, and friends.
* Closed tidyverse/dplyr#6154: Values from an `actionButton()` had S3 classes in the incorrect order.
* Closed #3346: Default for `ref` input in `runGithub()` changed from `"master"` to `"HEAD"`. (#3564)
* Closed #3619: In R 4.2, `splitLayout()` no longer raises warnings about incorrect length in an `if` statement. (Thanks to @dmenne, #3625)
### Bug fixes
* Closed #3250:`{rlang}`/`{tidyeval}` conditions (i.e., warnings and errors) are no longer filtered from stack traces. (#3602)
* Closed #3581: Errors in throttled/debounced reactive expressions no longer cause the session to exit. (#3624)
* Closed #3657: `throttle.ts` and the `Throttler` typescript objects it provides now function as intended. (Thanks gto @dvg-p4, #3659)
* The auto-reload feature (`options(shiny.autoreload=TRUE)`) was not being activated by `devmode(TRUE)`, despite a console message asserting that it was. (#3620)
* Closed #2297: If an error occurred in parsing a value in a bookmark query string, an error would be thrown and nothing would be restored. Now a message is displayed and that value is ignored. (Thanks to @daattali, #3385)
* Restored the previous behavior of automatically guessing the `Content-Type` header for `downloadHandler` functions when no explicit `contentType` argument is supplied. (#3393)
* Previously, updating an input value without a corresponding Input binding element did not trigger a JavaScript `shiny:inputchanged` event. Now, if no Input binding element is found, the `shiny:inputchanged` event is triggered on `window.document`. (#3584)
* Closed #2955: Input and output bindings previously attempted to use `el['data-input-id']`, but that never worked. They now use `el.getAttribute('data-input-id')` instead. (#3538)
### Minor improvements
* When taking a test snapshot, the sort order of the json keys of the `input`, `output`, and `export` fields is currently sorted using the locale of the machine. This can lead to inconsistent test snapshot results. To opt-in to a consistent ordering of snapshot fields with `{shinytest}`, please set the global option `options(shiny.snapshotsortc = TRUE)`. `{shinytest2}` users do not need to set this value. (#3515)
* Closed rstudio/shinytest2#222: When restoring a context (i.e., bookmarking) from a URL, Shiny now better handles a trailing `=` after `_inputs_` and `_values_`. (#3648)
* Shiny's internal HTML dependencies are now mounted dynamically instead of statically. (#3537)
* HTML dependencies that are sent to dynamic UI now have better type checking, and no longer require a `dep.src.href` field. (#3537)
# shiny 1.7.1
## Bug Fixes
* Closed #3516: Fix regression in repeated calls to `appendTab()` when `navbarMenu()` is already present within a `tabsetPanel()`/`navbarPage()`. (#3518)
* Re-arranged conditions for testthat 1.0.0 compatibility. (#3512)
# shiny 1.7.0
## Full changelog ## Full changelog
@@ -335,62 +7,30 @@ In addition, various properties of the spinners and pulse can be customized with
* The `format` and `locale` arguments to `sliderInput()` have been removed. They have been deprecated since 0.10.2.2 (released on 2014-12-08). * The `format` and `locale` arguments to `sliderInput()` have been removed. They have been deprecated since 0.10.2.2 (released on 2014-12-08).
* Closed #3403: `insertTab()`'s `position` parameter now defaults to `"after"` instead of `"before"`. This has the benefit of allowing us to fix a bug in positioning when `target = NULL`, but has the drawback of changing the default behavior when `target` is not `NULL`. (#3404) ### Minor new features and improvements
### New features and improvements * Shiny's core JavaScript code was converted to TypeScript. For the latest development information, please see the [README.md in `./srcts`](https://github.com/rstudio/shiny/tree/master/srcts). (#3296)
* Bootstrap 5 support. (#3410 and rstudio/bslib#304)
* As explained [here](https://rstudio.github.io/bslib/index.html#basic-usage), to opt-in to Bootstrap 5, provide `bslib::bs_theme(version = 5)` to a page layout function with a `theme` argument (e.g., `fluidPage()`, `navbarPage()`, etc).
* Closed #3322, #3313, #1823, #3321, #3320, #1928, and #2310: Various improvements to `navbarPage()`, `tabsetPanel()`, `tabPanel()`, `navbarMenu()`, etc. Also, these functions are now powered by the `{bslib}` package's new `nav()` API (consider using `{bslib}`'s API to create better looking and more fully featured navs). (#3388)
* All uses of `list(...)` have been replaced with `rlang::list2(...)`. This means that you can use trailing `,` without error and use rlang's `!!!` operator to "splice" a list of argument values into `...`. We think this'll be particularly useful for passing a list of `tabPanel()` to their consumers (i.e., `tabsetPanel()`, `navbarPage()`, etc). For example, `tabs <- list(tabPanel("A", "a"), tabPanel("B", "b")); navbarPage(!!!tabs)`. (#3315 and #3328)
* `installExprFunction()` and `exprToFunction()` are now able to handle quosures when `quoted = TRUE`. So `render`-functions which call these functions (such as with `htmlwidgets`) can now understand quosures. Users can also use `rlang::inject()` to unquote a quosure for evaluation. This also means that `render` function no longer need `env` and `quoted` parameters; that information can be embedded into a quosure which is then passed to the `render` function. Better documentation was added for how to create `render` functions. (#3472)
* `icon(lib="fontawesome")` is now powered by the `{fontawesome}` package, which will make it easier to use the latest FA icons in the future (by updating the `{fontawesome}` package). (#3302)
* Closed #3397: `renderPlot()` new uses `ggplot2::get_alt_text()` to inform an `alt` text default (for `{ggplot2}` plots). (#3398)
* `modalDialog()` gains support for `size = "xl"`. (#3410)
* Addressed #2521: Updated the list of TCP ports that will be rejected by default in runapp.R, adding 5060, 5061 and 6566. Added documentation describing the port range (3000:8000) and which ports are rejected. (#3456)
### Other improvements
* Shiny's core JavaScript code was converted to TypeScript. For the latest development information, please see the [README.md in `./srcts`](https://github.com/rstudio/shiny/tree/v1.7.0/srcts). (#3296)
* Switched from `digest::digest()` to `rlang::hash()` for hashing. (#3264) * Switched from `digest::digest()` to `rlang::hash()` for hashing. (#3264)
* Switched from internal `Stack` class to `fastmap::faststack()`, and used `fastmap::fastqueue()`. (#3176) * Switched from internal `Stack` class to `fastmap::faststack()`, and used `fastmap::fastqueue()`. (#3176)
* Some long-deprecated functions and function parameters were removed. (#3137)
### Bug fixes ### Bug fixes
* Closed #3345: Shiny now correctly renders `htmltools::htmlDependency()`(s) with a `list()` of `script` attributes when used in a dynamic UI context. This fairly new `htmlDependency()` feature was added in `{htmltools}` v0.5.1. (#3395)
* Fixed [#2666](https://github.com/rstudio/shiny/issues/2666) and [#2670](https://github.com/rstudio/shiny/issues/2670): `nearPoints()` and `brushedPoints()` weren't properly account for missing values (#2666 was introduced in v1.4.0). ([#2668](https://github.com/rstudio/shiny/pull/2668))
* Closed #3374: `quoToFunction()` now works correctly with nested quosures; and as a result, quasi-quotation with rendering function (e.g., `renderPrint()`, `renderPlot()`, etc) now works as expected with nested quosures. (#3373)
* Exported `register_devmode_option()`. This method was described in the documentation for `devmode()` but was never exported. See `?devmode()` for more details on how to register Shiny Developer options using `register_devmode_option()`. (#3364)
* Closed #3484: In the RStudio IDE on Mac 11.5, selected checkboxes and radio buttons were not visible. (#3485)
### Library updates ### Library updates
* Closed #3286: Updated to Font-Awesome 5.15.2. (#3288) * Closed #3286: Updated to Font-Awesome 5.15.2. (#3288)
* Updated to jQuery 3.6.0. (#3311) * Updated to jQuery 3.6.0. (#3311)
# shiny 1.6.0 shiny 1.6.0
===========
This release focuses on improvements in three main areas: This release focuses on improvements in three main areas:
1. Better theming (and Bootstrap 4) support: 1. Better theming (and Bootstrap 4) support:
* The `theme` argument of `fluidPage()`, `navbarPage()`, and `bootstrapPage()` all now understand `bslib::bs_theme()` objects, which can be used to opt-into Bootstrap 4, use any Bootswatch theme, and/or implement custom themes without writing any CSS. * The `theme` argument of `fluidPage()`, `navbarPage()`, and `bootstrapPage()` all now understand `bslib::bs_theme()` objects, which can be used to opt-into Bootstrap 4, use any Bootswatch theme, and/or implement custom themes without writing any CSS.
* The `session` object now includes `$setCurrentTheme()` and `$getCurrentTheme()` methods to dynamically update (or obtain) the page's `theme` after initial load, which is useful for things such as [adding a dark mode switch to an app](https://rstudio.github.io/bslib/articles/theming.html#dynamic) or some other "real-time" theming tool like `bslib::bs_themer()`. * The `session` object now includes `$setCurrentTheme()` and `$getCurrentTheme()` methods to dynamically update (or obtain) the page's `theme` after initial load, which is useful for things such as [adding a dark mode switch to an app](https://rstudio.github.io/bslib/articles/theming.html#dynamic-shiny) or some other "real-time" theming tool like `bslib::bs_themer()`.
* For more details, see [`{bslib}`'s website](https://rstudio.github.io/bslib/) * For more details, see [`{bslib}`'s website](https://rstudio.github.io/bslib/)
2. Caching of `reactive()` and `render*()` (e.g. `renderText()`, `renderTable()`, etc) expressions. 2. Caching of `reactive()` and `render*()` (e.g. `renderText()`, `renderTable()`, etc) expressions.
@@ -422,7 +62,7 @@ This release focuses on improvements in three main areas:
* Fixed #2951: screen readers correctly announce labels and date formats for `dateInput()` and `dateRangeInput()` widgets. (#2978) * Fixed #2951: screen readers correctly announce labels and date formats for `dateInput()` and `dateRangeInput()` widgets. (#2978)
* Closed #2847: `selectInput()` is reasonably accessible for screen readers even when `selectize` option is set to TRUE. To improve `selectize.js` accessibility, we have added [selectize-plugin-a11y](https://github.com/SalmenBejaoui/selectize-plugin-a11y) by default. (#2993) * Closed #2847: `selectInput()` is reasonably accessible for screen readers even when `selectize` option is set to TRUE. To improve `selectize.js` accessibility, we have added [selectize-plugin-a11y](https://github.com/SLMNBJ/selectize-plugin-a11y) by default. (#2993)
* Closed #612: Added `alt` argument to `renderPlot()` and `renderCachedPlot()` to specify descriptive texts for `plotOutput()` objects, which is essential for screen readers. By default, alt text is set to the static text, "Plot object," but even dynamic text can be made with reactive function. (#3006, thanks @trafficonese and @leonawicz for the original PR and discussion via #2494) * Closed #612: Added `alt` argument to `renderPlot()` and `renderCachedPlot()` to specify descriptive texts for `plotOutput()` objects, which is essential for screen readers. By default, alt text is set to the static text, "Plot object," but even dynamic text can be made with reactive function. (#3006, thanks @trafficonese and @leonawicz for the original PR and discussion via #2494)
@@ -493,7 +133,8 @@ This release focuses on improvements in three main areas:
* Removed es5-shim library, which was internally used within `selectInput()` for ECMAScript 5 compatibility. (#2993) * Removed es5-shim library, which was internally used within `selectInput()` for ECMAScript 5 compatibility. (#2993)
# shiny 1.5.0 shiny 1.5.0
===========
## Full changelog ## Full changelog
@@ -511,7 +152,7 @@ This release focuses on improvements in three main areas:
* The new `moduleServer` function provides a simpler interface for creating and using modules. (#2773) * The new `moduleServer` function provides a simpler interface for creating and using modules. (#2773)
* Resolved #2732: `markdown()` is a new function for writing Markdown with Github extensions directly in Shiny UIs. Markdown rendering is performed by the [commonmark](https://github.com/r-lib/commonmark) package. (#2737) * Resolved #2732: `markdown()` is a new function for writing Markdown with Github extensions directly in Shiny UIs. Markdown rendering is performed by the [commonmark](https://github.com/jeroen/commonmark) package. (#2737)
* The `getCurrentOutputInfo()` function can now return the background color (`bg`), foreground color (`fg`), `accent` (i.e., hyperlink) color, and `font` information of the output's HTML container. This information is reported by `plotOutput()`, `imageOutput()`, and any other output bindings containing a class of `.shiny-report-theme`. This feature allows developers to style an output's contents based on the container's CSS styling. (#2740) * The `getCurrentOutputInfo()` function can now return the background color (`bg`), foreground color (`fg`), `accent` (i.e., hyperlink) color, and `font` information of the output's HTML container. This information is reported by `plotOutput()`, `imageOutput()`, and any other output bindings containing a class of `.shiny-report-theme`. This feature allows developers to style an output's contents based on the container's CSS styling. (#2740)
@@ -546,17 +187,20 @@ This release focuses on improvements in three main areas:
* Updated from Font-Awesome 5.3.1 to 5.13.0, which includes icons related to COVID-19. For upgrade notes, see https://github.com/FortAwesome/Font-Awesome/blob/master/UPGRADING.md. (#2891) * Updated from Font-Awesome 5.3.1 to 5.13.0, which includes icons related to COVID-19. For upgrade notes, see https://github.com/FortAwesome/Font-Awesome/blob/master/UPGRADING.md. (#2891)
# shiny 1.4.0.2 shiny 1.4.0.2
===========
Minor patch release: fixed some timing-dependent tests failed intermittently on CRAN build machines. Minor patch release: fixed some timing-dependent tests failed intermittently on CRAN build machines.
# shiny 1.4.0.1 shiny 1.4.0.1
===========
Minor patch release to account for changes to the grid package that will be upcoming in the R 4.0 release (#2776). Minor patch release to account for changes to the grid package that will be upcoming in the R 4.0 release (#2776).
# shiny 1.4.0 shiny 1.4.0
===========
## Full changelog ## Full changelog
@@ -619,7 +263,8 @@ Minor patch release to account for changes to the grid package that will be upco
* Fixed #2329, #1817: These bugs were reported as fixed in Shiny 1.3.0 but were not actually fixed because some JavaScript changes were accidentally not included in the release. The fix resolves issues that occur when `withProgressBar()` or bookmarking are combined with the [networkD3](https://christophergandrud.github.io/networkD3/) package's Sankey plot. * Fixed #2329, #1817: These bugs were reported as fixed in Shiny 1.3.0 but were not actually fixed because some JavaScript changes were accidentally not included in the release. The fix resolves issues that occur when `withProgressBar()` or bookmarking are combined with the [networkD3](https://christophergandrud.github.io/networkD3/) package's Sankey plot.
# shiny 1.3.2 shiny 1.3.2
===========
### Bug fixes ### Bug fixes
@@ -628,7 +273,8 @@ Minor patch release to account for changes to the grid package that will be upco
* Fixed #2280: Shiny applications that used a www/index.html file did not serve up the index file. (#2382) * Fixed #2280: Shiny applications that used a www/index.html file did not serve up the index file. (#2382)
# shiny 1.3.1 shiny 1.3.1
===========
## Full changelog ## Full changelog
@@ -637,7 +283,8 @@ Minor patch release to account for changes to the grid package that will be upco
* Fixed a performance issue introduced in v1.3.0 when using large nested lists within Shiny. (#2377) * Fixed a performance issue introduced in v1.3.0 when using large nested lists within Shiny. (#2377)
# shiny 1.3.0 shiny 1.3.0
===========
## Full changelog ## Full changelog
@@ -668,7 +315,8 @@ Minor patch release to account for changes to the grid package that will be upco
* Fixed #2247: `renderCachedPlot` now supports using promises for either `expr` or `cacheKeyExpr`. (Shiny v1.2.0 supported async `expr`, but only if `cacheKeyExpr` was async as well; now you can use any combination of sync/async for `expr` and `cacheKeyExpr`.) #2261 * Fixed #2247: `renderCachedPlot` now supports using promises for either `expr` or `cacheKeyExpr`. (Shiny v1.2.0 supported async `expr`, but only if `cacheKeyExpr` was async as well; now you can use any combination of sync/async for `expr` and `cacheKeyExpr`.) #2261
# shiny 1.2.0 shiny 1.2.0
===========
This release features plot caching, an important new tool for improving performance and scalability. Using `renderCachedPlot` in place of `renderPlot` can greatly improve responsiveness for apps that show the same plot many times (for example, a dashboard or report where all users view the same data). Shiny gives you a fair amount of control in where the cache is stored and how cached plots are invalidated, so be sure to read [this article](https://shiny.rstudio.com/articles/plot-caching.html) to get the most out of this feature. This release features plot caching, an important new tool for improving performance and scalability. Using `renderCachedPlot` in place of `renderPlot` can greatly improve responsiveness for apps that show the same plot many times (for example, a dashboard or report where all users view the same data). Shiny gives you a fair amount of control in where the cache is stored and how cached plots are invalidated, so be sure to read [this article](https://shiny.rstudio.com/articles/plot-caching.html) to get the most out of this feature.
@@ -733,7 +381,8 @@ This release features plot caching, an important new tool for improving performa
* Addressed #1864 by changing `optgroup` documentation to use `list` instead of `c`. (#2084) * Addressed #1864 by changing `optgroup` documentation to use `list` instead of `c`. (#2084)
# shiny 1.1.0 shiny 1.1.0
===========
This is a significant release for Shiny, with a major new feature that was nearly a year in the making: support for asynchronous operations! Until now, R's single-threaded nature meant that performing long-running calculations or tasks from Shiny would bring your app to a halt for other users of that process. This release of Shiny deeply integrates the [promises](https://rstudio.github.io/promises/) package to allow you to execute some tasks asynchronously, including as part of reactive expressions and outputs. See the [promises](https://rstudio.github.io/promises/) documentation to learn more. This is a significant release for Shiny, with a major new feature that was nearly a year in the making: support for asynchronous operations! Until now, R's single-threaded nature meant that performing long-running calculations or tasks from Shiny would bring your app to a halt for other users of that process. This release of Shiny deeply integrates the [promises](https://rstudio.github.io/promises/) package to allow you to execute some tasks asynchronously, including as part of reactive expressions and outputs. See the [promises](https://rstudio.github.io/promises/) documentation to learn more.
@@ -769,7 +418,7 @@ This is a significant release for Shiny, with a major new feature that was nearl
* Removed the (ridiculously outdated) "experimental feature" tag from the reference documentation for `renderUI`. (#2036) * Removed the (ridiculously outdated) "experimental feature" tag from the reference documentation for `renderUI`. (#2036)
* Addressed #1907: the `ignoreInit` argument was first added only to `observeEvent`. Later, we also added it to `eventReactive`, but forgot to update the documentation. Now done, thanks @flo12392! (#2036) * Addressed #1907: the `ignoreInit` argument was first added only to `observeEvent`. Later, we also added it to `eventReactive`, but forgot to update the documentation. Now done, thanks [@flo12392](https://github.com/flo12392)! (#2036)
### Bug fixes ### Bug fixes
@@ -783,7 +432,7 @@ This is a significant release for Shiny, with a major new feature that was nearl
* Fixed #1600: URL-encoded bookmarking did not work with sliders that had dates or date-times. (#1961) * Fixed #1600: URL-encoded bookmarking did not work with sliders that had dates or date-times. (#1961)
* Fixed #1962: [File dragging and dropping](https://posit.co/blog/shiny-1-0-4/) broke in the presence of jQuery version 3.0 as introduced by the [rhandsontable](https://jrowen.github.io/rhandsontable/) [htmlwidget](https://www.htmlwidgets.org/). (#2005) * Fixed #1962: [File dragging and dropping](https://blog.rstudio.com/2017/08/15/shiny-1-0-4/) broke in the presence of jQuery version 3.0 as introduced by the [rhandsontable](https://jrowen.github.io/rhandsontable/) [htmlwidget](https://www.htmlwidgets.org/). (#2005)
* Improved the error handling inside the `addResourcePath()` function, to give end users more informative error messages when the `directoryPath` argument cannot be normalized. This is especially useful for `runtime: shiny_prerendered` Rmd documents, like `learnr` tutorials. (#1968) * Improved the error handling inside the `addResourcePath()` function, to give end users more informative error messages when the `directoryPath` argument cannot be normalized. This is especially useful for `runtime: shiny_prerendered` Rmd documents, like `learnr` tutorials. (#1968)
@@ -806,7 +455,8 @@ This is a significant release for Shiny, with a major new feature that was nearl
In some rare cases, interrupting an application (by pressing Ctrl-C or Esc) may result in the message `Error in execCallbacks(timeoutSecs) : c++ exception (unknown reason)`. Although this message sounds alarming, it is harmless, and will go away in a future version of the later package (more information [here](https://github.com/r-lib/later/issues/55)). In some rare cases, interrupting an application (by pressing Ctrl-C or Esc) may result in the message `Error in execCallbacks(timeoutSecs) : c++ exception (unknown reason)`. Although this message sounds alarming, it is harmless, and will go away in a future version of the later package (more information [here](https://github.com/r-lib/later/issues/55)).
# shiny 1.0.5 shiny 1.0.5
===========
## Full changelog ## Full changelog
@@ -819,7 +469,8 @@ In some rare cases, interrupting an application (by pressing Ctrl-C or Esc) may
* Fixed #1824: HTTP HEAD requests on static files caused the application to stop. (#1825) * Fixed #1824: HTTP HEAD requests on static files caused the application to stop. (#1825)
# shiny 1.0.4 shiny 1.0.4
===========
There are three headlining features in this release of Shiny. It is now possible to add and remove tabs from a `tabPanel`; there is a new function, `onStop()`, which registers callbacks that execute when an application exits; and `fileInput`s now can have files dragged and dropped on them. In addition to these features, this release has a number of minor features and bug fixes. See the full changelog below for more details. There are three headlining features in this release of Shiny. It is now possible to add and remove tabs from a `tabPanel`; there is a new function, `onStop()`, which registers callbacks that execute when an application exits; and `fileInput`s now can have files dragged and dropped on them. In addition to these features, this release has a number of minor features and bug fixes. See the full changelog below for more details.
@@ -880,7 +531,8 @@ There are three headlining features in this release of Shiny. It is now possible
* Fixed #1474: A `browser()` call in an observer could cause an error in the RStudio IDE on Windows. (#1802) * Fixed #1474: A `browser()` call in an observer could cause an error in the RStudio IDE on Windows. (#1802)
# shiny 1.0.3 shiny 1.0.3
================
This is a hotfix release of Shiny. With previous versions of Shiny, when running an application on the newly-released version of R, 3.4.0, it would print a message: `Warning in body(fun) : argument is not a function`. This has no effect on the application, but because the message could be alarming to users, we are releasing a new version of Shiny that fixes this issue. This is a hotfix release of Shiny. With previous versions of Shiny, when running an application on the newly-released version of R, 3.4.0, it would print a message: `Warning in body(fun) : argument is not a function`. This has no effect on the application, but because the message could be alarming to users, we are releasing a new version of Shiny that fixes this issue.
@@ -893,7 +545,8 @@ This is a hotfix release of Shiny. With previous versions of Shiny, when running
* Fixed #1676: On R 3.4.0, running a Shiny application gave a warning: `Warning in body(fun) : argument is not a function`. (#1677) * Fixed #1676: On R 3.4.0, running a Shiny application gave a warning: `Warning in body(fun) : argument is not a function`. (#1677)
# shiny 1.0.2 shiny 1.0.2
================
This is a hotfix release of Shiny. The primary reason for this release is because the web host for MathJax JavaScript library is scheduled to be shut down in the next few weeks. After it is shut down, Shiny applications that use MathJax will no longer be able to load the MathJax library if they are run with Shiny 1.0.1 and below. (If you don't know whether your application uses MathJax, it probably does not.) For more information about why the MathJax CDN is shutting down, see https://www.mathjax.org/cdn-shutting-down/. This is a hotfix release of Shiny. The primary reason for this release is because the web host for MathJax JavaScript library is scheduled to be shut down in the next few weeks. After it is shut down, Shiny applications that use MathJax will no longer be able to load the MathJax library if they are run with Shiny 1.0.1 and below. (If you don't know whether your application uses MathJax, it probably does not.) For more information about why the MathJax CDN is shutting down, see https://www.mathjax.org/cdn-shutting-down/.
@@ -912,7 +565,8 @@ This is a hotfix release of Shiny. The primary reason for this release is becaus
* Fixed #1653: wrong code example in documentation. (#1658) * Fixed #1653: wrong code example in documentation. (#1658)
# shiny 1.0.1 shiny 1.0.1
================
This is a maintenance release of Shiny, mostly aimed at fixing bugs and introducing minor features. The most notable additions in this version of Shiny are the introduction of the `reactiveVal()` function (it's like `reactiveValues()`, but it only stores a single value), and that the choices of `radioButtons()` and `checkboxGroupInput()` can now contain HTML content instead of just plain text. This is a maintenance release of Shiny, mostly aimed at fixing bugs and introducing minor features. The most notable additions in this version of Shiny are the introduction of the `reactiveVal()` function (it's like `reactiveValues()`, but it only stores a single value), and that the choices of `radioButtons()` and `checkboxGroupInput()` can now contain HTML content instead of just plain text.
@@ -982,7 +636,8 @@ in shiny apps. For more info, see the documentation (`?updateQueryString` and `?
* Closed #1500: Updated ion.rangeSlider to 2.1.6. (#1540) * Closed #1500: Updated ion.rangeSlider to 2.1.6. (#1540)
# shiny 1.0.0 shiny 1.0.0
===========
Shiny has reached a milestone: version 1.0.0! In the last year, we've added two major features that we considered essential for a 1.0.0 release: bookmarking, and support for testing Shiny applications. As usual, this version of Shiny also includes many minor features and bug fixes. Shiny has reached a milestone: version 1.0.0! In the last year, we've added two major features that we considered essential for a 1.0.0 release: bookmarking, and support for testing Shiny applications. As usual, this version of Shiny also includes many minor features and bug fixes.
@@ -1047,7 +702,8 @@ Now there's an official way to slow down reactive values and expressions that in
* Updated to Font Awesome 4.7.0. * Updated to Font Awesome 4.7.0.
# shiny 0.14.2 shiny 0.14.2
============
This is a maintenance release of Shiny, with some bug fixes and minor new features. This is a maintenance release of Shiny, with some bug fixes and minor new features.
@@ -1075,7 +731,8 @@ This is a maintenance release of Shiny, with some bug fixes and minor new featur
* 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) * 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)
# shiny 0.14.1 shiny 0.14.1
============
This is a maintenance release of Shiny, with some bug fixes and minor new features. This is a maintenance release of Shiny, with some bug fixes and minor new features.
@@ -1105,7 +762,8 @@ This is a maintenance release of Shiny, with some bug fixes and minor new featur
* 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) * 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)
# shiny 0.14 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! 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!
@@ -1126,7 +784,7 @@ Shiny can now display notifications on the client browser by using the `showNoti
<img src="http://shiny.rstudio.com/images/notification.png" alt="notification" width="50%"/> <img src="http://shiny.rstudio.com/images/notification.png" alt="notification" width="50%"/>
</p> </p>
[Here](https://shiny.rstudio.com/articles/notifications.html)'s our article about it, and the [reference documentation](https://shiny.posit.co/r/reference/shiny/latest/shownotification.html). [Here](https://shiny.rstudio.com/articles/notifications.html)'s our article about it, and the [reference documentation](https://shiny.rstudio.com/reference/shiny/latest/showNotification.html).
## Progress indicators ## Progress indicators
@@ -1135,7 +793,7 @@ If your Shiny app contains computations that take a long time to complete, a pro
**_Important note_:** **_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. > 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](https://shiny.rstudio.com/articles/progress.html) and in the reference documentation (either for the easier [`withProgress` functional API](https://shiny.posit.co/r/reference/shiny/latest/withprogress.html) or the more complicated, but more powerful, [`Progress` object-oriented API](https://shiny.posit.co/r/reference/shiny/latest/progress.html). 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](https://shiny.rstudio.com/articles/progress.html) and in the reference documentation (either for the easier [`withProgress` functional API](https://shiny.rstudio.com/reference/shiny/latest/withProgress.html) or the more complicated, but more powerful, [`Progress` object-oriented API](https://shiny.rstudio.com/reference/shiny/latest/Progress.html).
## Reconnection ## Reconnection
@@ -1149,7 +807,7 @@ Shiny has now built-in support for displaying modal dialogs like the one below (
<img src="http://shiny.rstudio.com/images/modal-dialog.png" alt="modal-dialog" width="50%"/> <img src="http://shiny.rstudio.com/images/modal-dialog.png" alt="modal-dialog" width="50%"/>
</p> </p>
To learn more about this, read [our article](https://shiny.rstudio.com/articles/modal-dialogs.html) and the [reference documentation](https://shiny.posit.co/r/reference/shiny/latest/modaldialog.html). To learn more about this, read [our article](https://shiny.rstudio.com/articles/modal-dialogs.html) and the [reference documentation](https://shiny.rstudio.com/reference/shiny/latest/modalDialog.html).
## `insertUI` and `removeUI` ## `insertUI` and `removeUI`
@@ -1157,7 +815,7 @@ Sometimes in a Shiny app, arbitrary HTML UI may need to be created on-the-fly in
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`. 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](https://shiny.rstudio.com/articles/dynamic-ui.html) about dynamic UI generation and the reference documentation about [`insertUI`](https://shiny.posit.co/r/reference/shiny/latest/insertui.html) and [`removeUI`](https://shiny.posit.co/r/reference/shiny/latest/insertui.html). For more, read [our article](https://shiny.rstudio.com/articles/dynamic-ui.html) about dynamic UI generation and the reference documentation about [`insertUI`](https://shiny.rstudio.com/reference/shiny/latest/insertUI.html) and [`removeUI`](https://shiny.rstudio.com/reference/shiny/latest/insertUI.html).
## Documentation for connecting to an external database ## Documentation for connecting to an external database
@@ -1191,7 +849,7 @@ There are many more minor features, small improvements, and bug fixes than we ca
<img src="http://shiny.rstudio.com/images/render-table.png" alt="render-table" width="75%"/> <img src="http://shiny.rstudio.com/images/render-table.png" alt="render-table" width="75%"/>
</p> </p>
For more, read our [short article](https://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](https://shiny.posit.co/r/reference/shiny/latest/rendertable.html). For more, read our [short article](https://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](https://shiny.rstudio.com/reference/shiny/latest/renderTable.html).
## Full changelog ## Full changelog
@@ -1304,12 +962,14 @@ There are many more minor features, small improvements, and bug fixes than we ca
* Updated to jQuery 1.12.4. * Updated to jQuery 1.12.4.
# shiny 0.13.2 shiny 0.13.2
============
* Updated documentation for `htmlTemplate`. * Updated documentation for `htmlTemplate`.
# shiny 0.13.1 shiny 0.13.1
============
* `flexCol` did not work on RStudio for Windows or Linux. * `flexCol` did not work on RStudio for Windows or Linux.
@@ -1318,7 +978,8 @@ There are many more minor features, small improvements, and bug fixes than we ca
* BREAKING CHANGE: The long-deprecated ability to pass functions (rather than expressions) to reactive() and observe() has finally been removed. * BREAKING CHANGE: The long-deprecated ability to pass functions (rather than expressions) to reactive() and observe() has finally been removed.
# shiny 0.13.0 shiny 0.13.0
============
* Fixed #962: plot interactions did not work with the development version of ggplot2 (after ggplot2 1.0.1). * Fixed #962: plot interactions did not work with the development version of ggplot2 (after ggplot2 1.0.1).
@@ -1369,7 +1030,8 @@ There are many more minor features, small improvements, and bug fixes than we ca
* 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. * 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 shiny 0.12.2
============
* GitHub changed URLs for gists from .tar.gz to .zip, so `runGist` was updated to work with the new URLs. * GitHub changed URLs for gists from .tar.gz to .zip, so `runGist` was updated to work with the new URLs.
@@ -1392,14 +1054,16 @@ There are many more minor features, small improvements, and bug fixes than we ca
* Shiny now correctly handles HTTP HEAD requests. (#876) * Shiny now correctly handles HTTP HEAD requests. (#876)
# shiny 0.12.1 shiny 0.12.1
============
* Fixed an issue where unbindAll() causes subsequent bindAll() to be ignored for previously bound outputs. (#856) * 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. * 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 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. In addition to the changes listed below (in the *Full Changelog* section), there is an infrastructure change that could affect existing Shiny apps.
@@ -1455,7 +1119,8 @@ Shiny 0.12.0 deprecated Shiny's dataTableOutput and renderDataTable functions an
* 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) * 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 shiny 0.11.1
============
* Major client-side performance improvements for pages that have many conditionalPanels, tabPanels, and plotOutputs. (#693, #717, #723) * Major client-side performance improvements for pages that have many conditionalPanels, tabPanels, and plotOutputs. (#693, #717, #723)
@@ -1482,7 +1147,8 @@ Shiny 0.12.0 deprecated Shiny's dataTableOutput and renderDataTable functions an
* 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). * 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
==========
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. 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.
@@ -1560,17 +1226,20 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Password input fields can now be used, with `passwordInput()`. (#672) * Password input fields can now be used, with `passwordInput()`. (#672)
# shiny 0.10.2.2 shiny 0.10.2.2
==============
* Remove use of `rstudio::viewer` in a code example, for R CMD check. * Remove use of `rstudio::viewer` in a code example, for R CMD check.
# shiny 0.10.2.1 shiny 0.10.2.1
==============
* Changed some examples to use \donttest instead of \dontrun. * Changed some examples to use \donttest instead of \dontrun.
# shiny 0.10.2 shiny 0.10.2
============
* The minimal version of R required for the shiny package is 3.0.0 now. * The minimal version of R required for the shiny package is 3.0.0 now.
@@ -1603,7 +1272,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Added `position` parameter to `navbarPage`. * Added `position` parameter to `navbarPage`.
# shiny 0.10.1 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) * 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)
@@ -1616,7 +1286,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* 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) * 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 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. * BREAKING CHANGE: By default, observers now terminate themselves if they were created during a session and that session ends. See ?domains for more details.
@@ -1653,12 +1324,14 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* `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. * `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 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". * 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 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). * 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).
@@ -1731,7 +1404,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Dots are now legal characters for inputId/outputId. (Thanks, Kevin Lindquist. #358) * Dots are now legal characters for inputId/outputId. (Thanks, Kevin Lindquist. #358)
# shiny 0.8.0 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. * 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.
@@ -1750,7 +1424,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* The minimal required version for the httpuv package was increased to 1.2 (on CRAN now). * The minimal required version for the httpuv package was increased to 1.2 (on CRAN now).
# shiny 0.7.0 shiny 0.7.0
===========
* Stopped sending websocket subprotocol. This fixes a compatibility issue with Google Chrome 30. * Stopped sending websocket subprotocol. This fixes a compatibility issue with Google Chrome 30.
@@ -1779,7 +1454,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Add shiny.sharedSecret option, to require the HTTP header Shiny-Shared-Secret to be set to the given value. * Add shiny.sharedSecret option, to require the HTTP header Shiny-Shared-Secret to be set to the given value.
# shiny 0.6.0 shiny 0.6.0
===========
* `tabsetPanel()` can be directed to start with a specific tab selected. * `tabsetPanel()` can be directed to start with a specific tab selected.
@@ -1810,7 +1486,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Shiny apps can be run without a server.r and ui.r file. * Shiny apps can be run without a server.r and ui.r file.
# shiny 0.5.0 shiny 0.5.0
===========
* Switch from websockets package for handling websocket connections to httpuv. * Switch from websockets package for handling websocket connections to httpuv.
@@ -1827,14 +1504,16 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Fix bug #55, where `renderTable()` would throw error with an empty data frame. * Fix bug #55, where `renderTable()` would throw error with an empty data frame.
# shiny 0.4.1 shiny 0.4.1
===========
* Fix bug where width and height weren't passed along properly from `reactivePlot` to `renderPlot`. * 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. * Fix bug where infinite recursion would happen when `reactivePlot` was passed a function for width or height.
# shiny 0.4.0 shiny 0.4.0
===========
* Added suspend/resume capability to observers. * Added suspend/resume capability to observers.
@@ -1849,7 +1528,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Fixed a bug where empty values in a numericInput were sent to the R process as 0. They are now sent as NA. * 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 shiny 0.3.1
===========
* Fix issue #91: bug where downloading files did not work. * Fix issue #91: bug where downloading files did not work.
@@ -1858,7 +1538,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Reactive functions now preserve the visible/invisible state of their returned values. * Reactive functions now preserve the visible/invisible state of their returned values.
# shiny 0.3.0 shiny 0.3.0
===========
* Reactive functions are now evaluated lazily. * Reactive functions are now evaluated lazily.
@@ -1883,44 +1564,52 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Fix issue #64, where pressing Enter in a textbox would cause a form to submit. * Fix issue #64, where pressing Enter in a textbox would cause a form to submit.
# shiny 0.2.4 shiny 0.2.4
===========
* `runGist` has been updated to use the new download URLs from https://gist.github.com. * `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 now uses `CairoPNG()` for output, when the Cairo package is available. This provides better-looking output on Linux and Windows.
# shiny 0.2.3 shiny 0.2.3
===========
* Ignore request variables for routing purposes * Ignore request variables for routing purposes
# shiny 0.2.2 shiny 0.2.2
===========
* Fix CRAN warning (assigning to global environment) * Fix CRAN warning (assigning to global environment)
# shiny 0.2.1 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. * [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 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 * 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 shiny 0.1.14
===========
* Fix slider animator, which broke in 0.1.10 * Fix slider animator, which broke in 0.1.10
# shiny 0.1.13 shiny 0.1.13
===========
* Fix temp file leak in reactivePlot * Fix temp file leak in reactivePlot
# shiny 0.1.12 shiny 0.1.12
===========
* Fix problems with runGist on Windows * Fix problems with runGist on Windows
@@ -1929,7 +1618,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Add CSS hooks for app-wide busy indicators * Add CSS hooks for app-wide busy indicators
# shiny 0.1.11 shiny 0.1.11
===========
* Fix input binding with IE8 on Shiny Server * Fix input binding with IE8 on Shiny Server
@@ -1938,7 +1628,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Allow dynamic sizing of reactivePlot (i.e. using a function instead of a fixed value) * Allow dynamic sizing of reactivePlot (i.e. using a function instead of a fixed value)
# shiny 0.1.10 shiny 0.1.10
===========
* Support more MIME types when serving out of www * Support more MIME types when serving out of www
@@ -1951,7 +1642,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Fix plot rendering with IE8 on Shiny Server * Fix plot rendering with IE8 on Shiny Server
# shiny 0.1.9 shiny 0.1.9
===========
* Much less flicker when updating plots * Much less flicker when updating plots
@@ -1960,7 +1652,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Add `includeText`, `includeHTML`, and `includeMarkdown` functions for putting text, HTML, and Markdown content from external files in the application's UI. * 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 shiny 0.1.8
===========
* Add `runGist` function for conveniently running a Shiny app that is published on gist.github.com. * Add `runGist` function for conveniently running a Shiny app that is published on gist.github.com.
@@ -1973,7 +1666,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Add `bootstrapPage` function for creating new Bootstrap based layouts from scratch. * Add `bootstrapPage` function for creating new Bootstrap based layouts from scratch.
# shiny 0.1.7 shiny 0.1.7
===========
* Fix issue #26: Shiny.OutputBindings not correctly exported. * Fix issue #26: Shiny.OutputBindings not correctly exported.
@@ -1982,7 +1676,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Transcode JSON into UTF-8 (prevents non-ASCII reactivePrint values from causing errors on Windows). * Transcode JSON into UTF-8 (prevents non-ASCII reactivePrint values from causing errors on Windows).
# shiny 0.1.6 shiny 0.1.6
===========
* Import package dependencies, instead of attaching them (with the exception of websockets, which doesn't currently work unless attached). * Import package dependencies, instead of attaching them (with the exception of websockets, which doesn't currently work unless attached).
@@ -1991,7 +1686,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* bindAll was not correctly sending initial values to the server; fixed. * bindAll was not correctly sending initial values to the server; fixed.
# shiny 0.1.5 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. * 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.
@@ -2006,7 +1702,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* htmlOutput (CSS class `shiny-html-output`) can contain inputs and outputs. * htmlOutput (CSS class `shiny-html-output`) can contain inputs and outputs.
# shiny 0.1.4 shiny 0.1.4
===========
* Allow Bootstrap tabsets to act as reactive inputs; their value indicates which tab is active * Allow Bootstrap tabsets to act as reactive inputs; their value indicates which tab is active
@@ -2019,7 +1716,8 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Add Shiny.bindInputs(scope), .unbindInputs(scope), .bindOutputs(scope), and .unbindOutputs(scope) JS API calls to allow dynamic binding/unbinding of HTML elements * 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 shiny 0.1.3
===========
* Introduce Shiny.inputBindings.register JS API and InputBinding class, for creating custom input controls * Introduce Shiny.inputBindings.register JS API and InputBinding class, for creating custom input controls
@@ -2032,6 +1730,7 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Fix issue #10: Plots in tabsets not rendered * Fix issue #10: Plots in tabsets not rendered
# shiny 0.1.2 shiny 0.1.2
===========
* Initial private beta release! * Initial private beta release!

View File

@@ -10,7 +10,8 @@
#' 2: app.R : Main application file #' 2: app.R : Main application file
#' 3: R/example.R : Helper file with R code #' 3: R/example.R : Helper file with R code
#' 4: R/example-module.R : Example module #' 4: R/example-module.R : Example module
#' 5: tests/testthat/ : Tests using the testthat and shinytest2 package #' 5: tests/shinytest/ : Tests using the shinytest package
#' 6: tests/testthat/ : Tests using the testthat package
#' ``` #' ```
#' #'
#' If option 1 is selected, the full example application including the #' If option 1 is selected, the full example application including the
@@ -23,12 +24,13 @@
#' | |- example-module.R #' | |- example-module.R
#' | `- example.R #' | `- example.R
#' `- tests #' `- tests
#' |- shinytest.R
#' |- shinytest
#' | `- mytest.R
#' |- testthat.R #' |- testthat.R
#' `- testthat #' `- testthat
#' |- setup-shinytest2.R
#' |- test-examplemodule.R #' |- test-examplemodule.R
#' |- test-server.R #' |- test-server.R
#' |- test-shinytest2.R
#' `- test-sort.R #' `- test-sort.R
#' ``` #' ```
#' #'
@@ -43,21 +45,20 @@
#' * `tests/` contains various tests for the application. You may #' * `tests/` contains various tests for the application. You may
#' choose to use or remove any of them. They can be executed by the #' choose to use or remove any of them. They can be executed by the
#' [runTests()] function. #' [runTests()] function.
#' * `tests/shinytest.R` is a test runner for test files in the
#' `tests/shinytest/` directory.
#' * `tests/shinytest/mytest.R` is a test that uses the
#' [shinytest](https://rstudio.github.io/shinytest/) package to do
#' snapshot-based testing.
#' * `tests/testthat.R` is a test runner for test files in the #' * `tests/testthat.R` is a test runner for test files in the
#' `tests/testthat/` directory using the #' `tests/testthat/` directory using the [testthat](https://testthat.r-lib.org/) package.
#' [shinytest2](https://rstudio.github.io/shinytest2/reference/test_app.html)
#' package.
#' * `tests/testthat/setup-shinytest2.R` is setup file to source your `./R` folder into the testing environment.
#' * `tests/testthat/test-examplemodule.R` is a test for an application's module server function. #' * `tests/testthat/test-examplemodule.R` is a test for an application's module server function.
#' * `tests/testthat/test-server.R` is a test for the application's server code #' * `tests/testthat/test-server.R` is a test for the application's server code
#' * `tests/testthat/test-shinytest2.R` is a test that uses the
#' [shinytest2](https://rstudio.github.io/shinytest2/) package to do
#' snapshot-based testing.
#' * `tests/testthat/test-sort.R` is a test for a supporting function in the `R/` directory. #' * `tests/testthat/test-sort.R` is a test for a supporting function in the `R/` directory.
#' #'
#' @param path Path to create new shiny application template. #' @param path Path to create new shiny application template.
#' @param examples Either one of "default", "ask", "all", or any combination of #' @param examples Either one of "default", "ask", "all", or any combination of
#' "app", "rdir", "module", and "tests". In an #' "app", "rdir", "module", "shinytest", and "testthat". In an
#' interactive session, "default" falls back to "ask"; in a non-interactive #' interactive session, "default" falls back to "ask"; in a non-interactive
#' session, "default" falls back to "all". With "ask", this function will #' session, "default" falls back to "all". With "ask", this function will
#' prompt the user to select which template items will be added to the new app #' prompt the user to select which template items will be added to the new app
@@ -78,19 +79,15 @@ shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
# ======================================================= # =======================================================
choices <- c( choices <- c(
app = "app.R : Main application file", app = "app.R : Main application file",
rdir = "R/example.R : Helper file with R code", rdir = "R/example.R : Helper file with R code",
module = "R/example-module.R : Example module", module = "R/example-module.R : Example module",
tests = "tests/testthat/ : Tests using {testthat} and {shinytest2}" shinytest = "tests/shinytest/ : Tests using the shinytest package",
testthat = "tests/testthat/ : Tests using the testthat package"
) )
# Support legacy value
examples[examples == "shinytest"] <- "tests"
examples[examples == "testthat"] <- "tests"
examples <- unique(examples)
if (identical(examples, "default")) { if (identical(examples, "default")) {
if (rlang::is_interactive()) { if (interactive()) {
examples <- "ask" examples <- "ask"
} else { } else {
examples <- "all" examples <- "all"
@@ -127,8 +124,18 @@ shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
return(invisible()) return(invisible())
} }
if ("tests" %in% examples) { if ("shinytest" %in% examples) {
rlang::check_installed("shinytest2", "for {testthat} tests to work as expected", version = "0.2.0") if (!is_available("shinytest", "1.4.0"))
{
message(
"The tests/shinytest directory needs shinytest 1.4.0 or later to work properly."
)
if (is_available("shinytest")) {
message("You currently have shinytest ",
utils::packageVersion("shinytest"), " installed.")
}
}
} }
# ======================================================= # =======================================================
@@ -145,7 +152,7 @@ shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
# Helper to resolve paths relative to our template # Helper to resolve paths relative to our template
template_path <- function(...) { template_path <- function(...) {
system_file("app_template", ..., package = "shiny") system.file("app_template", ..., package = "shiny")
} }
# Resolve path relative to destination # Resolve path relative to destination
@@ -201,13 +208,16 @@ shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
} }
# Copy the files for a tests/ subdirectory # Copy the files for a tests/ subdirectory
copy_test_dir <- function() { copy_test_dir <- function(name) {
files <- dir(template_path("tests"), recursive = TRUE) files <- dir(template_path("tests"), recursive = TRUE)
# Note: This is not the same as using dir(pattern = "^shinytest"), since
# that will not match files inside of shinytest/.
files <- files[grepl(paste0("^", name), files)]
# Filter out files that are not module files in the R directory. # Filter out files that are not module files in the R directory.
if (! "rdir" %in% examples) { if (! "rdir" %in% examples) {
# find all files in the testthat folder that are not module or server files # find all files in the testthat folder that are not module or server files
is_r_folder_file <- !grepl("module|server|shinytest2|testthat", basename(files)) is_r_folder_file <- (!grepl("module|server", basename(files))) & (dirname(files) == "testthat")
files <- files[!is_r_folder_file] files <- files[!is_r_folder_file]
} }
@@ -272,10 +282,12 @@ shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
copy_file(file.path("R", module_files)) copy_file(file.path("R", module_files))
} }
# tests/testthat dir # tests/ dir
if ("tests" %in% examples) { if ("shinytest" %in% examples) {
copy_test_dir() copy_test_dir("shinytest")
}
if ("testthat" %in% examples) {
copy_test_dir("testthat")
} }
invisible() invisible()
} }

View File

@@ -159,8 +159,8 @@ utils::globalVariables(".GenericCallEnv", add = TRUE)
#' ``` #' ```
#' #'
#' To use different settings for a session-scoped cache, you can set #' To use different settings for a session-scoped cache, you can set
#' `session$cache` at the top of your server function. By default, it will #' `self$cache` at the top of your server function. By default, it will create
#' create a 200 MB memory cache for each session, but you can replace it with #' a 200 MB memory cache for each session, but you can replace it with
#' something different. To use the session-scoped cache, you must also call #' something different. To use the session-scoped cache, you must also call
#' `bindCache()` with `cache="session"`. This will create a 100 MB cache for #' `bindCache()` with `cache="session"`. This will create a 100 MB cache for
#' the session: #' the session:
@@ -177,7 +177,7 @@ utils::globalVariables(".GenericCallEnv", add = TRUE)
#' cache by putting this at the top of your app.R, server.R, or global.R: #' cache by putting this at the top of your app.R, server.R, or global.R:
#' #'
#' ``` #' ```
#' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))) #' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))
#' ``` #' ```
#' #'
#' This will create a subdirectory in your system temp directory named #' This will create a subdirectory in your system temp directory named
@@ -231,8 +231,8 @@ utils::globalVariables(".GenericCallEnv", add = TRUE)
#' promises, but rather objects provided by the #' promises, but rather objects provided by the
#' \href{https://rstudio.github.io/promises/}{\pkg{promises}} package, which #' \href{https://rstudio.github.io/promises/}{\pkg{promises}} package, which
#' are similar to promises in JavaScript. (See [promises::promise()] for more #' are similar to promises in JavaScript. (See [promises::promise()] for more
#' information.) You can also use [mirai::mirai()] or [future::future()] #' information.) You can also use [future::future()] objects to run code in a
#' objects to run code in a separate process or even on a remote machine. #' separate process or even on a remote machine.
#' #'
#' If the value returns a promise, then anything that consumes the cached #' If the value returns a promise, then anything that consumes the cached
#' reactive must expect it to return a promise. #' reactive must expect it to return a promise.
@@ -255,7 +255,7 @@ utils::globalVariables(".GenericCallEnv", add = TRUE)
#' the cache. #' the cache.
#' #'
#' You may need to provide a `cacheHint` to [createRenderFunction()] (or #' You may need to provide a `cacheHint` to [createRenderFunction()] (or
#' `htmlwidgets::shinyRenderWidget()`, if you've authored an htmlwidget) in #' [htmlwidgets::shinyRenderWidget()], if you've authored an htmlwidget) in
#' order for `bindCache()` to correctly compute a cache key. #' order for `bindCache()` to correctly compute a cache key.
#' #'
#' The potential problem is a cache collision. Consider the following: #' The potential problem is a cache collision. Consider the following:
@@ -292,11 +292,11 @@ utils::globalVariables(".GenericCallEnv", add = TRUE)
#' In some cases, however, the automatic cache hint inference is not #' In some cases, however, the automatic cache hint inference is not
#' sufficient, and it is necessary to provide a cache hint. This is true #' sufficient, and it is necessary to provide a cache hint. This is true
#' for `renderPrint()`. Unlike `renderText()`, it wraps the user-provided #' for `renderPrint()`. Unlike `renderText()`, it wraps the user-provided
#' expression in another function, before passing it to [createRenderFunction()] #' expression in another function, before passing it to [markRenderFunction()]
#' (instead of [createRenderFunction()]). Because the user code is wrapped in #' (instead of [createRenderFunction()]). Because the user code is wrapped in
#' another function, `createRenderFunction()` is not able to automatically #' another function, `markRenderFunction()` is not able to automatically
#' extract the user-provided code and use it in the cache key. Instead, #' extract the user-provided code and use it in the cache key. Instead,
#' `renderPrint` calls `createRenderFunction()`, it explicitly passes along a #' `renderPrint` calls `markRenderFunction()`, it explicitly passes along a
#' `cacheHint`, which includes a label and the original user expression. #' `cacheHint`, which includes a label and the original user expression.
#' #'
#' In general, if you need to provide a `cacheHint`, it is best practice to #' In general, if you need to provide a `cacheHint`, it is best practice to
@@ -310,19 +310,19 @@ utils::globalVariables(".GenericCallEnv", add = TRUE)
#' #'
#' ``` #' ```
#' renderMyWidget <- function(expr) { #' renderMyWidget <- function(expr) {
#' q <- rlang::enquo0(expr) #' expr <- substitute(expr)
#' #'
#' htmlwidgets::shinyRenderWidget( #' htmlwidgets::shinyRenderWidget(expr,
#' q,
#' myWidgetOutput, #' myWidgetOutput,
#' quoted = TRUE, #' quoted = TRUE,
#' cacheHint = list(label = "myWidget", userQuo = q) #' env = parent.frame(),
#' cacheHint = list(label = "myWidget", userExpr = expr)
#' ) #' )
#' } #' }
#' ``` #' ```
#' #'
#' If your `render` function sets any internal state, you may find it useful #' If your `render` function sets any internal state, you may find it useful
#' in your call to [createRenderFunction()] to use #' in your call to [createRenderFunction()] or [markRenderFunction()] to use
#' the `cacheWriteHook` and/or `cacheReadHook` parameters. These hooks are #' the `cacheWriteHook` and/or `cacheReadHook` parameters. These hooks are
#' functions that run just before the object is stored in the cache, and just #' functions that run just before the object is stored in the cache, and just
#' after the object is retrieved from the cache. They can modify the data #' after the object is retrieved from the cache. They can modify the data
@@ -339,8 +339,8 @@ utils::globalVariables(".GenericCallEnv", add = TRUE)
#' effects or modify some external state, and they must re-execute each time #' effects or modify some external state, and they must re-execute each time
#' in order to work properly. #' in order to work properly.
#' #'
#' For developers of such code, they should call [createRenderFunction()] (or #' For developers of such code, they should call [createRenderFunction()] or
#' [markRenderFunction()]) with `cacheHint = FALSE`. #' [markRenderFunction()] with `cacheHint = FALSE`.
#' #'
#' #'
#' @section Caching with `renderPlot()`: #' @section Caching with `renderPlot()`:
@@ -453,7 +453,7 @@ utils::globalVariables(".GenericCallEnv", add = TRUE)
#' bindEvent(input$go) #' bindEvent(input$go)
#' # The cached, eventified reactive takes a reactive dependency on #' # The cached, eventified reactive takes a reactive dependency on
#' # input$go, but doesn't use it for the cache key. It uses input$x and #' # input$go, but doesn't use it for the cache key. It uses input$x and
#' # input$y for the cache key, but doesn't take a reactive dependency on #' # input$y for the cache key, but doesn't take a reactive depdency on
#' # them, because the reactive dependency is superseded by addEvent(). #' # them, because the reactive dependency is superseded by addEvent().
#' #'
#' output$txt <- renderText(r()) #' output$txt <- renderText(r())

View File

@@ -99,13 +99,13 @@ saveShinySaveState <- function(state) {
# Encode the state to a URL. This does not save to disk. # Encode the state to a URL. This does not save to disk.
encodeShinySaveState <- function(state) { 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. # Allow user-supplied onSave function to do things like add state$values.
if (!is.null(state$onSave)) if (!is.null(state$onSave))
isolate(state$onSave(state)) isolate(state$onSave(state))
exclude <- c(state$exclude, "._bookmark_")
inputVals <- serializeReactiveValues(state$input, exclude, stateDir = NULL)
inputVals <- vapply(inputVals, inputVals <- vapply(inputVals,
function(x) toJSON(x, strict_atomic = FALSE), function(x) toJSON(x, strict_atomic = FALSE),
character(1), character(1),
@@ -321,38 +321,34 @@ RestoreContext <- R6Class("RestoreContext",
if (substr(queryString, 1, 1) == '?') if (substr(queryString, 1, 1) == '?')
queryString <- substr(queryString, 2, nchar(queryString)) queryString <- substr(queryString, 2, nchar(queryString))
# The "=" after "_inputs_" is optional. Shiny doesn't generate URLs with
# "=", but httr always adds "=".
inputs_reg <- "(^|&)_inputs_=?(&|$)"
values_reg <- "(^|&)_values_=?(&|$)"
# Error if multiple '_inputs_' or '_values_'. This is needed because # 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 # strsplit won't add an entry if the search pattern is at the end of a
# string. # string.
if (length(gregexpr(inputs_reg, queryString)[[1]]) > 1) if (length(gregexpr("(^|&)_inputs_(&|$)", queryString)[[1]]) > 1)
stop("Invalid state string: more than one '_inputs_' found") stop("Invalid state string: more than one '_inputs_' found")
if (length(gregexpr(values_reg, queryString)[[1]]) > 1) if (length(gregexpr("(^|&)_values_(&|$)", queryString)[[1]]) > 1)
stop("Invalid state string: more than one '_values_' found") stop("Invalid state string: more than one '_values_' found")
# Look for _inputs_ and store following content in inputStr # Look for _inputs_ and store following content in inputStr
splitStr <- strsplit(queryString, inputs_reg)[[1]] splitStr <- strsplit(queryString, "(^|&)_inputs_(&|$)")[[1]]
if (length(splitStr) == 2) { if (length(splitStr) == 2) {
inputStr <- splitStr[2] inputStr <- splitStr[2]
# Remove any _values_ (and content after _values_) that may come after # Remove any _values_ (and content after _values_) that may come after
# _inputs_ # _inputs_
inputStr <- strsplit(inputStr, values_reg)[[1]][1] inputStr <- strsplit(inputStr, "(^|&)_values_(&|$)")[[1]][1]
} else { } else {
inputStr <- "" inputStr <- ""
} }
# Look for _values_ and store following content in valueStr # Look for _values_ and store following content in valueStr
splitStr <- strsplit(queryString, values_reg)[[1]] splitStr <- strsplit(queryString, "(^|&)_values_(&|$)")[[1]]
if (length(splitStr) == 2) { if (length(splitStr) == 2) {
valueStr <- splitStr[2] valueStr <- splitStr[2]
# Remove any _inputs_ (and content after _inputs_) that may come after # Remove any _inputs_ (and content after _inputs_) that may come after
# _values_ # _values_
valueStr <- strsplit(valueStr, inputs_reg)[[1]][1] valueStr <- strsplit(valueStr, "(^|&)_inputs_(&|$)")[[1]][1]
} else { } else {
valueStr <- "" valueStr <- ""
@@ -363,20 +359,16 @@ RestoreContext <- R6Class("RestoreContext",
values <- parseQueryString(valueStr, nested = TRUE) values <- parseQueryString(valueStr, nested = TRUE)
valuesFromJSON <- function(vals) { valuesFromJSON <- function(vals) {
varsUnparsed <- c() mapply(names(vals), vals, SIMPLIFY = FALSE,
valsParsed <- mapply(names(vals), vals, SIMPLIFY = FALSE,
FUN = function(name, value) { FUN = function(name, value) {
tryCatch( tryCatch(
safeFromJSON(value), safeFromJSON(value),
error = function(e) { error = function(e) {
varsUnparsed <<- c(varsUnparsed, name) stop("Failed to parse URL parameter \"", name, "\"")
warning("Failed to parse URL parameter \"", name, "\"")
} }
) )
} }
) )
valsParsed[varsUnparsed] <- NULL
valsParsed
} }
inputs <- valuesFromJSON(inputs) inputs <- valuesFromJSON(inputs)
@@ -452,10 +444,8 @@ RestoreInputSet <- R6Class("RestoreInputSet",
) )
) )
# This is a fastmap::faststack(); value is assigned in .onLoad().
restoreCtxStack <- NULL restoreCtxStack <- NULL
on_load({
restoreCtxStack <- fastmap::faststack()
})
withRestoreContext <- function(ctx, expr) { withRestoreContext <- function(ctx, expr) {
restoreCtxStack$push(ctx) restoreCtxStack$push(ctx)
@@ -551,7 +541,7 @@ restoreInput <- function(id, default) {
#' `window.history.pushState(null, null, queryString)`. #' `window.history.pushState(null, null, queryString)`.
#' #'
#' @param queryString The new query string to show in the location bar. #' @param queryString The new query string to show in the location bar.
#' @param mode When the query string is updated, should the current history #' @param mode When the query string is updated, should the the current history
#' entry be replaced (default), or should a new history entry be pushed onto #' entry be replaced (default), or should a new history entry be pushed onto
#' the history stack? The former should only be used in a live bookmarking #' the history stack? The former should only be used in a live bookmarking
#' context. The latter is useful if you want to navigate between states using #' context. The latter is useful if you want to navigate between states using

View File

@@ -6,7 +6,7 @@
#' @param sidebarPanel The [sidebarPanel] containing input controls #' @param sidebarPanel The [sidebarPanel] containing input controls
#' @param mainPanel The [mainPanel] containing outputs #' @param mainPanel The [mainPanel] containing outputs
#' @keywords internal #' @keywords internal
#' @return A UI definition that can be passed to the [shinyUI] function #' @return A UI defintion that can be passed to the [shinyUI] function
#' @export #' @export
pageWithSidebar <- function(headerPanel, pageWithSidebar <- function(headerPanel,
sidebarPanel, sidebarPanel,

View File

@@ -11,9 +11,11 @@
#' @param ... Elements to include within the page #' @param ... Elements to include within the page
#' @param title The browser window title (defaults to the host URL of the page). #' @param title The browser window title (defaults to the host URL of the page).
#' Can also be set as a side effect of the [titlePanel()] function. #' Can also be set as a side effect of the [titlePanel()] function.
#' @param responsive This option is deprecated; it is no longer optional with
#' Bootstrap 3.
#' @inheritParams bootstrapPage #' @inheritParams bootstrapPage
#' #'
#' @return A UI definition that can be passed to the [shinyUI] function. #' @return A UI defintion that can be passed to the [shinyUI] function.
#' #'
#' @details To create a fluid page use the `fluidPage` function and include #' @details To create a fluid page use the `fluidPage` function and include
#' instances of `fluidRow` and [column()] within it. As an #' instances of `fluidRow` and [column()] within it. As an
@@ -83,9 +85,10 @@
#' } #' }
#' @rdname fluidPage #' @rdname fluidPage
#' @export #' @export
fluidPage <- function(..., title = NULL, theme = NULL, lang = NULL) { fluidPage <- function(..., title = NULL, responsive = deprecated(), theme = NULL, lang = NULL) {
bootstrapPage(div(class = "container-fluid", ...), bootstrapPage(div(class = "container-fluid", ...),
title = title, title = title,
responsive = responsive,
theme = theme, theme = theme,
lang = lang) lang = lang)
} }
@@ -109,9 +112,11 @@ fluidRow <- function(...) {
#' #'
#' @param ... Elements to include within the container #' @param ... Elements to include within the container
#' @param title The browser window title (defaults to the host URL of the page) #' @param title The browser window title (defaults to the host URL of the page)
#' @param responsive This option is deprecated; it is no longer optional with
#' Bootstrap 3.
#' @inheritParams bootstrapPage #' @inheritParams bootstrapPage
#' #'
#' @return A UI definition that can be passed to the [shinyUI] function. #' @return A UI defintion that can be passed to the [shinyUI] function.
#' #'
#' @details To create a fixed page use the `fixedPage` function and include #' @details To create a fixed page use the `fixedPage` function and include
#' instances of `fixedRow` and [column()] within it. Note that #' instances of `fixedRow` and [column()] within it. Note that
@@ -148,9 +153,10 @@ fluidRow <- function(...) {
#' #'
#' @rdname fixedPage #' @rdname fixedPage
#' @export #' @export
fixedPage <- function(..., title = NULL, theme = NULL, lang = NULL) { fixedPage <- function(..., title = NULL, responsive = deprecated(), theme = NULL, lang = NULL) {
bootstrapPage(div(class = "container", ...), bootstrapPage(div(class = "container", ...),
title = title, title = title,
responsive = responsive,
theme = theme, theme = theme,
lang = lang) lang = lang)
} }
@@ -390,7 +396,7 @@ mainPanel <- function(..., width = 8) {
#' } #' }
#' @export #' @export
verticalLayout <- function(..., fluid = TRUE) { verticalLayout <- function(..., fluid = TRUE) {
lapply(list2(...), function(row) { lapply(list(...), function(row) {
col <- column(12, row) col <- column(12, row)
if (fluid) if (fluid)
fluidRow(col) fluidRow(col)
@@ -427,7 +433,7 @@ verticalLayout <- function(..., fluid = TRUE) {
#' @export #' @export
flowLayout <- function(..., cellArgs = list()) { flowLayout <- function(..., cellArgs = list()) {
children <- list2(...) children <- list(...)
childIdx <- !nzchar(names(children) %||% character(length(children))) childIdx <- !nzchar(names(children) %||% character(length(children)))
attribs <- children[!childIdx] attribs <- children[!childIdx]
children <- children[childIdx] children <- children[childIdx]
@@ -510,13 +516,13 @@ inputPanel <- function(...) {
#' @export #' @export
splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) { splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
children <- list2(...) children <- list(...)
childIdx <- !nzchar(names(children) %||% character(length(children))) childIdx <- !nzchar(names(children) %||% character(length(children)))
attribs <- children[!childIdx] attribs <- children[!childIdx]
children <- children[childIdx] children <- children[childIdx]
count <- length(children) count <- length(children)
if (length(cellWidths) == 0 || isTRUE(is.na(cellWidths))) { if (length(cellWidths) == 0 || is.na(cellWidths)) {
cellWidths <- sprintf("%.3f%%", 100 / count) cellWidths <- sprintf("%.3f%%", 100 / count)
} }
cellWidths <- rep(cellWidths, length.out = count) cellWidths <- rep(cellWidths, length.out = count)
@@ -608,7 +614,7 @@ fillCol <- function(..., flex = 1, width = "100%", height = "100%") {
} }
flexfill <- function(..., direction, flex, width = width, height = height) { flexfill <- function(..., direction, flex, width = width, height = height) {
children <- list2(...) children <- list(...)
attrs <- list() attrs <- list()
if (!is.null(names(children))) { if (!is.null(names(children))) {

View File

@@ -14,6 +14,8 @@ NULL
#' #'
#' @param ... The contents of the document body. #' @param ... The contents of the document body.
#' @param title The browser window title (defaults to the host URL of the page) #' @param title The browser window title (defaults to the host URL of the page)
#' @param responsive This option is deprecated; it is no longer optional with
#' Bootstrap 3.
#' @param theme One of the following: #' @param theme One of the following:
#' * `NULL` (the default), which implies a "stock" build of Bootstrap 3. #' * `NULL` (the default), which implies a "stock" build of Bootstrap 3.
#' * A [bslib::bs_theme()] object. This can be used to replace a stock #' * A [bslib::bs_theme()] object. This can be used to replace a stock
@@ -24,24 +26,30 @@ NULL
#' This will be used as the lang in the \code{<html>} tag, as in \code{<html lang="en">}. #' This will be used as the lang in the \code{<html>} tag, as in \code{<html lang="en">}.
#' The default (NULL) results in an empty string. #' The default (NULL) results in an empty string.
#' #'
#' @return A UI definition that can be passed to the [shinyUI] function. #' @return A UI defintion that can be passed to the [shinyUI] function.
#' #'
#' @note The `basicPage` function is deprecated, you should use the #' @note The `basicPage` function is deprecated, you should use the
#' [fluidPage()] function instead. #' [fluidPage()] function instead.
#' #'
#' @seealso [fluidPage()], [fixedPage()] #' @seealso [fluidPage()], [fixedPage()]
#' @export #' @export
bootstrapPage <- function(..., title = NULL, theme = NULL, lang = NULL) { bootstrapPage <- function(..., title = NULL, responsive = deprecated(), theme = NULL, lang = NULL) {
if (lifecycle::is_present(responsive)) {
shinyDeprecated(
"0.10.2.2", "bootstrapPage(responsive=)",
details = "The 'responsive' argument is no longer used with the latest version of Bootstrap."
)
}
args <- list( args <- list(
jqueryDependency(),
if (!is.null(title)) tags$head(tags$title(title)), if (!is.null(title)) tags$head(tags$title(title)),
if (is.character(theme)) { if (is.character(theme)) {
if (length(theme) > 1) stop("`theme` must point to a single CSS file, not multiple files.") if (length(theme) > 1) stop("`theme` must point to a single CSS file, not multiple files.")
tags$head(tags$link(rel="stylesheet", type="text/css", href=theme)) tags$head(tags$link(rel="stylesheet", type="text/css", href=theme))
}, },
# remainder of tags passed to the function # remainder of tags passed to the function
list2(...) list(...)
) )
# If theme is a bslib::bs_theme() object, bootstrapLib() needs to come first # If theme is a bslib::bs_theme() object, bootstrapLib() needs to come first
@@ -83,10 +91,6 @@ getLang <- function(ui) {
#' @export #' @export
bootstrapLib <- function(theme = NULL) { bootstrapLib <- function(theme = NULL) {
tagFunction(function() { tagFunction(function() {
if (isRunning()) {
setCurrentTheme(theme)
}
# If we're not compiling Bootstrap Sass (from bslib), return the # If we're not compiling Bootstrap Sass (from bslib), return the
# static Bootstrap build. # static Bootstrap build.
if (!is_bs_theme(theme)) { if (!is_bs_theme(theme)) {
@@ -108,6 +112,7 @@ bootstrapLib <- function(theme = NULL) {
# Note also that since this is shinyOptions() (and not options()), the # Note also that since this is shinyOptions() (and not options()), the
# option is automatically reset when the app (or session) exits # option is automatically reset when the app (or session) exits
if (isRunning()) { if (isRunning()) {
setCurrentTheme(theme)
registerThemeDependency(bs_theme_deps) registerThemeDependency(bs_theme_deps)
} else { } else {
@@ -138,7 +143,8 @@ bs_theme_deps <- function(theme) {
} }
is_bs_theme <- function(x) { is_bs_theme <- function(x) {
bslib::is_bs_theme(x) is_available("bslib", "0.2.0.9000") &&
bslib::is_bs_theme(x)
} }
#' Obtain Shiny's Bootstrap Sass theme #' Obtain Shiny's Bootstrap Sass theme
@@ -157,25 +163,15 @@ getCurrentTheme <- function() {
getShinyOption("bootstrapTheme", default = NULL) getShinyOption("bootstrapTheme", default = NULL)
} }
getCurrentThemeVersion <- function() {
theme <- getCurrentTheme()
if (bslib::is_bs_theme(theme)) {
bslib::theme_version(theme)
} else {
strsplit(bootstrapVersion, ".", fixed = TRUE)[[1]][[1]]
}
}
setCurrentTheme <- function(theme) { setCurrentTheme <- function(theme) {
shinyOptions(bootstrapTheme = theme) shinyOptions(bootstrapTheme = theme)
} }
#' Register a theme dependency #' Register a theme dependency
#' #'
#' This function registers a function that returns an #' This function registers a function that returns an [htmlDependency()] or list
#' [htmltools::htmlDependency()] or list of such objects. If #' of such objects. If `session$setCurrentTheme()` is called, the function will
#' `session$setCurrentTheme()` is called, the function will be re-executed, and #' be re-executed, and the resulting html dependency will be sent to the client.
#' the resulting html dependency will be sent to the client.
#' #'
#' Note that `func` should **not** be an anonymous function, or a function which #' Note that `func` should **not** be an anonymous function, or a function which
#' is defined within the calling function. This is so that, #' is defined within the calling function. This is so that,
@@ -215,10 +211,11 @@ registerThemeDependency <- function(func) {
bootstrapDependency <- function(theme) { bootstrapDependency <- function(theme) {
htmlDependency( htmlDependency(
"bootstrap", "bootstrap", "3.4.1",
bootstrapVersion, c(
src = "www/shared/bootstrap", href = "shared/bootstrap",
package = "shiny", file = system.file("www/shared/bootstrap", package = "shiny")
),
script = c( script = c(
"js/bootstrap.min.js", "js/bootstrap.min.js",
# Safely adding accessibility plugin for screen readers and keyboard users; no break for sighted aspects (see https://github.com/paypal/bootstrap-accessibility-plugin) # Safely adding accessibility plugin for screen readers and keyboard users; no break for sighted aspects (see https://github.com/paypal/bootstrap-accessibility-plugin)
@@ -233,8 +230,6 @@ bootstrapDependency <- function(theme) {
) )
} }
bootstrapVersion <- "3.4.1"
#' @rdname bootstrapPage #' @rdname bootstrapPage
#' @export #' @export
@@ -347,20 +342,13 @@ collapseSizes <- function(padding) {
#' Create a page with a top level navigation bar #' Create a page with a top level navigation bar
#' #'
#' Create a page that contains a top level navigation bar that can be used to #' Create a page that contains a top level navigation bar that can be used to
#' toggle a set of [tabPanel()] elements. #' toggle a set of [tabPanel()] elements. `navbarMenu()` can be used to create
#' an embedded menu within the navbar that in turns includes additional
#' `tabPanels`.
#' #'
#' @inheritParams navlistPanel
#' @inheritParams bootstrapPage
#' @param title The title to display in the navbar #' @param title The title to display in the navbar
#' @param ... [tabPanel()] elements to include in the page. The
#' `navbarMenu` function also accepts strings, which will be used as menu
#' section headers. If the string is a set of dashes like `"----"` a
#' horizontal separator will be displayed in the menu.
#' @param id If provided, you can use `input$`*`id`* in your
#' server logic to determine which of the current tabs is active. The value
#' will correspond to the `value` argument that is passed to
#' [tabPanel()].
#' @param selected The `value` (or, if none was supplied, the `title`)
#' of the tab that should be selected by default. If `NULL`, the first
#' tab will be selected.
#' @param position Determines whether the navbar should be displayed at the top #' @param position Determines whether the navbar should be displayed at the top
#' of the page with normal scrolling behavior (`"static-top"`), pinned at #' of the page with normal scrolling behavior (`"static-top"`), pinned at
#' the top (`"fixed-top"`), or pinned at the bottom #' the top (`"fixed-top"`), or pinned at the bottom
@@ -375,27 +363,16 @@ collapseSizes <- function(padding) {
#' @param inverse `TRUE` to use a dark background and light text for the #' @param inverse `TRUE` to use a dark background and light text for the
#' navigation bar #' navigation bar
#' @param collapsible `TRUE` to automatically collapse the navigation #' @param collapsible `TRUE` to automatically collapse the navigation
#' elements into an expandable menu on mobile devices or narrow window widths. #' elements into a menu when the width of the browser is less than 940 pixels
#' @param fluid `TRUE` to use a fluid layout. `FALSE` to use a fixed #' (useful for viewing on smaller touchscreen device)
#' layout. #' @param collapsable Deprecated; use `collapsible` instead.
#' @param windowTitle the browser window title (as a character string). The #' @param windowTitle The title that should be displayed by the browser window.
#' default value, `NA`, means to use any character strings that appear in #' Useful if `title` is not a string.
#' `title` (if none are found, the host URL of the page is displayed by
#' default).
#' @inheritParams bootstrapPage
#' @param icon Optional icon to appear on a `navbarMenu` tab. #' @param icon Optional icon to appear on a `navbarMenu` tab.
#' #'
#' @return A UI definition that can be passed to the [shinyUI] function. #' @seealso [updateNavbarPage()], [insertTab()], [showTab()]
#'
#' @details The `navbarMenu` function can be used to create an embedded
#' menu within the navbar that in turns includes additional tabPanels (see
#' example below).
#'
#' @seealso [tabPanel()], [tabsetPanel()],
#' [updateNavbarPage()], [insertTab()],
#' [showTab()]
#'
#' @family layout functions #' @family layout functions
#' @family tab layouts
#' #'
#' @examples #' @examples
#' navbarPage("App Title", #' navbarPage("App Title",
@@ -423,20 +400,86 @@ navbarPage <- function(title,
footer = NULL, footer = NULL,
inverse = FALSE, inverse = FALSE,
collapsible = FALSE, collapsible = FALSE,
collapsable = deprecated(),
fluid = TRUE, fluid = TRUE,
responsive = deprecated(),
theme = NULL, theme = NULL,
windowTitle = NA, windowTitle = title,
lang = NULL) { lang = NULL) {
remove_first_class(bslib::page_navbar(
..., title = title, id = id, selected = selected, if (lifecycle::is_present(collapsable)) {
position = match.arg(position), shinyDeprecated("0.10.2.2", "navbarPage(collapsable =)", "navbarPage(collapsible =)")
header = header, footer = footer, collapsible <- collapsable
inverse = inverse, collapsible = collapsible, }
fluid = fluid,
# alias title so we can avoid conflicts w/ title in withTags
pageTitle <- title
# navbar class based on options
navbarClass <- "navbar navbar-default"
position <- match.arg(position)
if (!is.null(position))
navbarClass <- paste(navbarClass, " navbar-", position, sep = "")
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, selected)
# function to return plain or fluid class name
className <- function(name) {
if (fluid)
paste(name, "-fluid", sep="")
else
name
}
# built the container div dynamically to support optional collapsibility
if (collapsible) {
navId <- paste("navbar-collapse-", p_randomInt(1000, 10000), sep="")
containerDiv <- div(class=className("container"),
div(class="navbar-header",
tags$button(type="button", class="navbar-toggle collapsed",
`data-toggle`="collapse", `data-target`=paste0("#", navId),
span(class="sr-only", "Toggle navigation"),
span(class="icon-bar"),
span(class="icon-bar"),
span(class="icon-bar")
),
span(class="navbar-brand", pageTitle)
),
div(class="navbar-collapse collapse", id=navId, tabset$navList)
)
} else {
containerDiv <- div(class=className("container"),
div(class="navbar-header",
span(class="navbar-brand", pageTitle)
),
tabset$navList
)
}
# build the main tab content div
contentDiv <- div(class=className("container"))
if (!is.null(header))
contentDiv <- tagAppendChild(contentDiv, div(class="row", header))
contentDiv <- tagAppendChild(contentDiv, tabset$content)
if (!is.null(footer))
contentDiv <- tagAppendChild(contentDiv, div(class="row", footer))
# build the page
bootstrapPage(
title = windowTitle,
responsive = responsive,
theme = theme, theme = theme,
window_title = windowTitle, lang = lang,
lang = lang tags$nav(class=navbarClass, role="navigation", containerDiv),
)) contentDiv
)
} }
#' @param menuName A name that identifies this `navbarMenu`. This #' @param menuName A name that identifies this `navbarMenu`. This
@@ -446,7 +489,11 @@ navbarPage <- function(title,
#' @rdname navbarPage #' @rdname navbarPage
#' @export #' @export
navbarMenu <- function(title, ..., menuName = title, icon = NULL) { navbarMenu <- function(title, ..., menuName = title, icon = NULL) {
bslib::nav_menu(title, ..., value = menuName, icon = icon) structure(list(title = title,
menuName = menuName,
tabs = list(...),
iconClass = iconClass(icon)),
class = "shiny.navbarmenu")
} }
#' Create a well panel #' Create a well panel
@@ -533,12 +580,7 @@ wellPanel <- function(...) {
#' } #' }
#' @export #' @export
conditionalPanel <- function(condition, ..., ns = NS(NULL)) { conditionalPanel <- function(condition, ..., ns = NS(NULL)) {
div( div(`data-display-if`=condition, `data-ns-prefix`=ns(""), ...)
class = "shiny-panel-conditional",
`data-display-if` = condition,
`data-ns-prefix` = ns(""),
...
)
} }
#' Create a help text element #' Create a help text element
@@ -561,6 +603,10 @@ helpText <- function(...) {
#' Create a tab panel #' Create a tab panel
#' #'
#' `tabPanel()` creates a tab panel that can be included within a
#' [tabsetPanel()], [navListPanel()], or [navbarPage()]. `tabPanelBody()`
#' drops the `title`, making it most suitable for use within
#' `tabsetPanel(type = "hidden")`.
#' #'
#' @param title Display title for tab #' @param title Display title for tab
#' @param ... UI elements to include within the tab #' @param ... UI elements to include within the tab
@@ -569,9 +615,8 @@ helpText <- function(...) {
#' `id`, then the title will be used. #' `id`, then the title will be used.
#' @param icon Optional icon to appear on the tab. This attribute is only #' @param icon Optional icon to appear on the tab. This attribute is only
#' valid when using a `tabPanel` within a [navbarPage()]. #' valid when using a `tabPanel` within a [navbarPage()].
#' @return A tab that can be passed to [tabsetPanel()]
#' #'
#' @seealso [tabsetPanel()] #' @family tab layouts
#' #'
#' @examples #' @examples
#' # Show a tabset that includes a plot, summary, and #' # Show a tabset that includes a plot, summary, and
@@ -584,16 +629,27 @@ helpText <- function(...) {
#' ) #' )
#' ) #' )
#' @export #' @export
#' @describeIn tabPanel Create a tab panel that can be included within a [tabsetPanel()] or a [navbarPage()].
tabPanel <- function(title, ..., value = title, icon = NULL) { tabPanel <- function(title, ..., value = title, icon = NULL) {
bslib::nav(title, ..., value = value, icon = icon) div(
class = "tab-pane",
title = title,
`data-value` = value,
`data-icon-class` = iconClass(icon),
...
)
} }
#' @export #' @export
#' @describeIn tabPanel Create a tab panel that drops the title argument. #' @rdname tabPanel
#' This function should be used within `tabsetPanel(type = "hidden")`. See [tabsetPanel()] for example usage.
tabPanelBody <- function(value, ..., icon = NULL) { tabPanelBody <- function(value, ..., icon = NULL) {
bslib::nav_content(value, ..., icon = icon) if (
!is.character(value) ||
length(value) != 1 ||
any(is.na(value)) ||
nchar(value) == 0
) {
stop("`value` must be a single, non-empty string value")
}
tabPanel(title = NULL, ..., value = value, icon = icon)
} }
#' Create a tabset panel #' Create a tabset panel
@@ -616,11 +672,10 @@ tabPanelBody <- function(value, ..., icon = NULL) {
#' conjunction with [tabPanelBody()] and [updateTabsetPanel()] to control the #' conjunction with [tabPanelBody()] and [updateTabsetPanel()] to control the
#' active tab via other input controls. (See example below)} #' active tab via other input controls. (See example below)}
#' } #' }
#' @inheritParams navbarPage #' @param position This argument is deprecated; it has been discontinued in
#' @return A tabset that can be passed to [mainPanel()] #' Bootstrap 3.
#' #' @seealso [updateTabsetPanel()], [insertTab()], [showTab()]
#' @seealso [tabPanel()], [updateTabsetPanel()], #' @family tab layouts
#' [insertTab()], [showTab()]
#' #'
#' @examples #' @examples
#' # Show a tabset that includes a plot, summary, and #' # Show a tabset that includes a plot, summary, and
@@ -666,21 +721,29 @@ tabsetPanel <- function(...,
id = NULL, id = NULL,
selected = NULL, selected = NULL,
type = c("tabs", "pills", "hidden"), type = c("tabs", "pills", "hidden"),
header = NULL, position = deprecated()) {
footer = NULL) { if (lifecycle::is_present(position)) {
shinyDeprecated(
"0.10.2.2", "bootstrapPage(position =)",
details = "The 'position' argument is no longer used with the latest version of Bootstrap."
)
}
func <- switch( if (!is.null(id))
match.arg(type), selected <- restoreInput(id = id, default = selected)
tabs = bslib::navs_tab,
pills = bslib::navs_pill,
hidden = bslib::navs_hidden
)
# bslib adds a class to make the content browsable() by default, # build the tabset
# but that's probably too big of a change for shiny tabs <- list(...)
remove_first_class( type <- match.arg(type)
func(..., id = id, selected = selected, header = header, footer = footer)
) tabset <- buildTabset(tabs, paste0("nav nav-", type), NULL, id, selected)
# create the content
first <- tabset$navList
second <- tabset$content
# create the tab div
tags$div(class = "tabbable", first, second)
} }
#' Create a navigation list panel #' Create a navigation list panel
@@ -688,31 +751,20 @@ tabsetPanel <- function(...,
#' Create a navigation list panel that provides a list of links on the left #' Create a navigation list panel that provides a list of links on the left
#' which navigate to a set of tabPanels displayed to the right. #' which navigate to a set of tabPanels displayed to the right.
#' #'
#' @param ... [tabPanel()] elements to include in the navlist #' @inheritParams tabsetPanel
#' @param id If provided, you can use `input$`*`id`* in your #' @param ... [tabPanel()] elements to include in the navbar.
#' server logic to determine which of the current navlist items is active. The #' Plain strings will be converted to headers.
#' value will correspond to the `value` argument that is passed to
#' [tabPanel()].
#' @param selected The `value` (or, if none was supplied, the `title`)
#' of the navigation item that should be selected by default. If `NULL`,
#' the first navigation will be selected.
#' @param well `TRUE` to place a well (gray rounded rectangle) around the #' @param well `TRUE` to place a well (gray rounded rectangle) around the
#' navigation list. #' navigation list.
#' @param fluid `TRUE` to use fluid layout; `FALSE` to use fixed #' @param fluid `TRUE` to use fluid layout; `FALSE` to use fixed
#' layout. #' layout.
#' @param widths Column widths of the navigation list and tabset content areas #' @param widths Column widths of the navigation list and tabset content areas
#' respectively. #' respectively.
#' @inheritParams tabsetPanel
#' @inheritParams navbarPage
#' #'
#' @details You can include headers within the `navlistPanel` by including #' @details
#' plain text elements in the list. Versions of Shiny before 0.11 supported
#' separators with "------", but as of 0.11, separators were no longer
#' supported. This is because version 0.11 switched to Bootstrap 3, which
#' doesn't support separators.
#' #'
#' @seealso [tabPanel()], [updateNavlistPanel()], #' @seealso [updateTabsetPanel()], [insertTab()], [showTab()]
#' [insertTab()], [showTab()] #' @family tab layouts
#' #'
#' @examples #' @examples
#' fluidPage( #' fluidPage(
@@ -730,23 +782,194 @@ tabsetPanel <- function(...,
navlistPanel <- function(..., navlistPanel <- function(...,
id = NULL, id = NULL,
selected = NULL, selected = NULL,
header = NULL,
footer = NULL,
well = TRUE, well = TRUE,
fluid = TRUE, fluid = TRUE,
widths = c(4, 8)) { widths = c(4, 8)) {
remove_first_class(bslib::navs_pill_list(
..., id = id, selected = selected, # text filter for headers
header = header, footer = footer, textFilter <- function(text) {
well = well, fluid = fluid, widths = widths tags$li(class="navbar-brand", text)
)) }
if (!is.null(id))
selected <- restoreInput(id = id, default = selected)
# build the tabset
tabs <- list(...)
tabset <- buildTabset(tabs,
"nav nav-pills nav-stacked",
textFilter,
id,
selected)
# create the columns
columns <- list(
column(widths[[1]], class=ifelse(well, "well", ""), tabset$navList),
column(widths[[2]], tabset$content)
)
# return the row
if (fluid)
fluidRow(columns)
else
fixedRow(columns)
} }
remove_first_class <- function(x) { # Helpers to build tabsetPanels (& Co.) and their elements
class(x) <- class(x)[-1] markTabAsSelected <- function(x) {
attr(x, "selected") <- TRUE
x x
} }
isTabSelected <- function(x) {
isTRUE(attr(x, "selected", exact = TRUE))
}
containsSelectedTab <- function(tabs) {
any(vapply(tabs, isTabSelected, logical(1)))
}
findAndMarkSelectedTab <- function(tabs, selected, foundSelected) {
tabs <- lapply(tabs, function(div) {
if (foundSelected || is.character(div)) {
# Strings are not selectable items
} else if (inherits(div, "shiny.navbarmenu")) {
# Recur for navbarMenus
res <- findAndMarkSelectedTab(div$tabs, selected, foundSelected)
div$tabs <- res$tabs
foundSelected <<- res$foundSelected
} else {
# Base case: regular tab item. If the `selected` argument is
# provided, check for a match in the existing tabs; else,
# mark first available item as selected
if (is.null(selected)) {
foundSelected <<- TRUE
div <- markTabAsSelected(div)
} else {
tabValue <- div$attribs$`data-value` %||% div$attribs$title
if (identical(selected, tabValue)) {
foundSelected <<- TRUE
div <- markTabAsSelected(div)
}
}
}
return(div)
})
return(list(tabs = tabs, foundSelected = foundSelected))
}
# Returns the icon object (or NULL if none), provided either a
# tabPanel, OR the icon class
getIcon <- function(tab = NULL, iconClass = NULL) {
if (!is.null(tab)) iconClass <- tab$attribs$`data-icon-class`
if (!is.null(iconClass)) {
if (grepl("fa-", iconClass, fixed = TRUE)) {
# for font-awesome we specify fixed-width
iconClass <- paste(iconClass, "fa-fw")
}
icon(name = NULL, class = iconClass)
} else NULL
}
# Text filter for navbarMenu's (plain text) separators
navbarMenuTextFilter <- function(text) {
if (grepl("^\\-+$", text)) tags$li(class = "divider")
else tags$li(class = "dropdown-header", text)
}
# This function is called internally by navbarPage, tabsetPanel
# and navlistPanel
buildTabset <- function(tabs, ulClass, textFilter = NULL, id = NULL,
selected = NULL, foundSelected = FALSE) {
res <- findAndMarkSelectedTab(tabs, selected, foundSelected)
tabs <- res$tabs
foundSelected <- res$foundSelected
# add input 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 = ", "))
}
tabsetId <- p_randomInt(1000, 10000)
tabs <- lapply(seq_len(length(tabs)), buildTabItem,
tabsetId = tabsetId, foundSelected = foundSelected,
tabs = tabs, textFilter = textFilter)
tabNavList <- tags$ul(class = ulClass, id = id,
`data-tabsetid` = tabsetId, lapply(tabs, "[[", 1))
tabContent <- tags$div(class = "tab-content",
`data-tabsetid` = tabsetId, lapply(tabs, "[[", 2))
list(navList = tabNavList, content = tabContent)
}
# Builds tabPanel/navbarMenu items (this function used to be
# declared inside the buildTabset() function and it's been
# refactored for clarity and reusability). Called internally
# by buildTabset.
buildTabItem <- function(index, tabsetId, foundSelected, tabs = NULL,
divTag = NULL, textFilter = NULL) {
divTag <- if (!is.null(divTag)) divTag else tabs[[index]]
if (is.character(divTag) && !is.null(textFilter)) {
# text item: pass it to the textFilter if it exists
liTag <- textFilter(divTag)
divTag <- NULL
} else if (inherits(divTag, "shiny.navbarmenu")) {
# navbarMenu item: build the child tabset
tabset <- buildTabset(divTag$tabs, "dropdown-menu",
navbarMenuTextFilter, foundSelected = foundSelected)
# if this navbarMenu contains a selected item, mark it active
containsSelected <- containsSelectedTab(divTag$tabs)
liTag <- tags$li(
class = paste0("dropdown", if (containsSelected) " active"),
tags$a(href = "#",
class = "dropdown-toggle", `data-toggle` = "dropdown",
`data-value` = divTag$menuName,
getIcon(iconClass = divTag$iconClass),
divTag$title, tags$b(class = "caret")
),
tabset$navList # inner tabPanels items
)
# list of tab content divs from the child tabset
divTag <- tabset$content$children
} else {
# tabPanel item: create the tab's liTag and divTag
tabId <- paste("tab", tabsetId, index, sep = "-")
liTag <- tags$li(
tags$a(
href = paste("#", tabId, sep = ""),
`data-toggle` = "tab",
`data-value` = divTag$attribs$`data-value`,
getIcon(iconClass = divTag$attribs$`data-icon-class`),
divTag$attribs$title
)
)
# if this tabPanel is selected item, mark it active
if (isTabSelected(divTag)) {
liTag$attribs$class <- "active"
divTag$attribs$class <- "tab-pane active"
}
divTag$attribs$id <- tabId
divTag$attribs$title <- NULL
}
return(list(liTag = liTag, divTag = divTag))
}
#' Create a text output element #' Create a text output element
#' #'
#' Render a reactive output variable as text within an application page. #' Render a reactive output variable as text within an application page.
@@ -799,7 +1022,7 @@ verbatimTextOutput <- function(outputId, placeholder = FALSE) {
#' @export #' @export
imageOutput <- function(outputId, width = "100%", height="400px", imageOutput <- function(outputId, width = "100%", height="400px",
click = NULL, dblclick = NULL, hover = NULL, brush = NULL, click = NULL, dblclick = NULL, hover = NULL, brush = NULL,
inline = FALSE, fill = FALSE) { inline = FALSE) {
style <- if (!inline) { style <- if (!inline) {
# Using `css()` here instead of paste/sprintf so that NULL values will # Using `css()` here instead of paste/sprintf so that NULL values will
@@ -855,8 +1078,7 @@ imageOutput <- function(outputId, width = "100%", height="400px",
} }
container <- if (inline) span else div container <- if (inline) span else div
res <- do.call(container, args) do.call(container, args)
bindFillRole(res, item = fill)
} }
#' Create an plot or image output element #' Create an plot or image output element
@@ -924,11 +1146,6 @@ imageOutput <- function(outputId, width = "100%", height="400px",
#' `imageOutput`/`plotOutput` calls may share the same `id` #' `imageOutput`/`plotOutput` calls may share the same `id`
#' value; brushing one image or plot will cause any other brushes with the #' value; brushing one image or plot will cause any other brushes with the
#' same `id` to disappear. #' same `id` to disappear.
#' @param fill Whether or not the returned tag should be treated as a fill item,
#' meaning that its `height` is allowed to grow/shrink to fit a fill container
#' with an opinionated height (see [htmltools::bindFillRole()]) with an
#' opinionated height. Examples of fill containers include `bslib::card()` and
#' `bslib::card_body_fill()`.
#' @inheritParams textOutput #' @inheritParams textOutput
#' @note The arguments `clickId` and `hoverId` only work for R base graphics #' @note The arguments `clickId` and `hoverId` only work for R base graphics
#' (see the \pkg{\link[graphics:graphics-package]{graphics}} package). They do #' (see the \pkg{\link[graphics:graphics-package]{graphics}} package). They do
@@ -1099,11 +1316,11 @@ imageOutput <- function(outputId, width = "100%", height="400px",
#' @export #' @export
plotOutput <- function(outputId, width = "100%", height="400px", plotOutput <- function(outputId, width = "100%", height="400px",
click = NULL, dblclick = NULL, hover = NULL, brush = NULL, click = NULL, dblclick = NULL, hover = NULL, brush = NULL,
inline = FALSE, fill = !inline) { inline = FALSE) {
# Result is the same as imageOutput, except for HTML class # Result is the same as imageOutput, except for HTML class
res <- imageOutput(outputId, width, height, click, dblclick, res <- imageOutput(outputId, width, height, click, dblclick,
hover, brush, inline, fill) hover, brush, inline)
res$attribs$class <- "shiny-plot-output" res$attribs$class <- "shiny-plot-output"
res res
@@ -1113,23 +1330,17 @@ plotOutput <- function(outputId, width = "100%", height="400px",
#' @rdname renderTable #' @rdname renderTable
#' @export #' @export
tableOutput <- function(outputId) { tableOutput <- function(outputId) {
div(id = outputId, class="shiny-html-output shiny-table-output") div(id = outputId, class="shiny-html-output")
} }
dataTableDependency <- list( dataTableDependency <- list(
htmlDependency( htmlDependency(
"datatables", "datatables", "1.10.5", c(href = "shared/datatables"),
"1.10.22",
src = "www/shared/datatables",
package = "shiny",
script = "js/jquery.dataTables.min.js" script = "js/jquery.dataTables.min.js"
), ),
htmlDependency( htmlDependency(
"datatables-bootstrap", "datatables-bootstrap", "1.10.5", c(href = "shared/datatables"),
"1.10.22", stylesheet = c("css/dataTables.bootstrap.css", "css/dataTables.extra.css"),
src = "www/shared/datatables",
package = "shiny",
stylesheet = "css/dataTables.bootstrap.css",
script = "js/dataTables.bootstrap.js" script = "js/dataTables.bootstrap.js"
) )
) )
@@ -1137,67 +1348,24 @@ dataTableDependency <- list(
#' @rdname renderDataTable #' @rdname renderDataTable
#' @export #' @export
dataTableOutput <- function(outputId) { dataTableOutput <- function(outputId) {
legacy <- useLegacyDataTable(from = "shiny::dataTableOutput()", to = "DT::DTOutput()") attachDependencies(
div(id = outputId, class="shiny-datatable-output"),
if (legacy) { dataTableDependency
attachDependencies( )
div(id = outputId, class = "shiny-datatable-output"),
dataTableDependency
)
} else {
DT::DTOutput(outputId)
}
} }
useLegacyDataTable <- function(from, to) {
legacy <- getOption("shiny.legacy.datatable")
# If option has been set, user knows what they're doing
if (!is.null(legacy)) {
return(legacy)
}
# If not set, use DT if a suitable version is available (and inform either way)
hasDT <- is_installed("DT", "0.32.1")
details <- NULL
if (hasDT) {
details <- paste0(c(
"Since you have a suitable version of DT (>= v0.32.1), ",
from,
" will automatically use ",
to,
" under-the-hood.\n",
"If this happens to break your app, set `options(shiny.legacy.datatable = TRUE)` ",
"to get the legacy datatable implementation (or `FALSE` to squelch this message).\n"
), collapse = "")
}
details <- paste0(details, "See <https://rstudio.github.io/DT/shiny.html> for more information.")
shinyDeprecated("1.8.1", from, to, details)
!hasDT
}
#' Create an HTML output element #' Create an HTML output element
#' #'
#' Render a reactive output variable as HTML within an application page. The #' Render a reactive output variable as HTML within an application page. The
#' text will be included within an HTML `div` tag, and is presumed to contain #' text will be included within an HTML `div` tag, and is presumed to
#' HTML content which should not be escaped. #' contain HTML content which should not be escaped.
#' #'
#' `uiOutput` is intended to be used with `renderUI` on the server side. It is #' `uiOutput` is intended to be used with `renderUI` on the server
#' currently just an alias for `htmlOutput`. #' side. It is currently just an alias for `htmlOutput`.
#' #'
#' @param outputId output variable to read the value from #' @param outputId output variable to read the value from
#' @param ... Other arguments to pass to the container tag function. This is #' @param ... Other arguments to pass to the container tag function. This is
#' useful for providing additional classes for the tag. #' useful for providing additional classes for the tag.
#' @param fill If `TRUE`, the result of `container` is treated as _both_ a fill
#' item and container (see [htmltools::bindFillRole()]), which means both the
#' `container` as well as its immediate children (i.e., the result of
#' `renderUI()`) are allowed to grow/shrink to fit a fill container with an
#' opinionated height. Set `fill = "item"` or `fill = "container"` to treat
#' `container` as just a fill item or a fill container.
#' @inheritParams textOutput #' @inheritParams textOutput
#' @return An HTML output element that can be included in a panel #' @return An HTML output element that can be included in a panel
#' @examples #' @examples
@@ -1209,16 +1377,12 @@ useLegacyDataTable <- function(from, to) {
#' ) #' )
#' @export #' @export
htmlOutput <- function(outputId, inline = FALSE, htmlOutput <- function(outputId, inline = FALSE,
container = if (inline) span else div, fill = FALSE, ...) container = if (inline) span else div, ...)
{ {
if (any_unnamed(list(...))) { if (anyUnnamed(list(...))) {
warning("Unnamed elements in ... will be replaced with dynamic UI.") warning("Unnamed elements in ... will be replaced with dynamic UI.")
} }
res <- container(id = outputId, class = "shiny-html-output", ...) container(id = outputId, class="shiny-html-output", ...)
bindFillRole(
res, item = isTRUE(fill) || isTRUE("item" == fill),
container = isTRUE(fill) || isTRUE("container" == fill)
)
} }
#' @rdname htmlOutput #' @rdname htmlOutput
@@ -1242,25 +1406,19 @@ uiOutput <- htmlOutput
#' @examples #' @examples
#' \dontrun{ #' \dontrun{
#' ui <- fluidPage( #' ui <- fluidPage(
#' p("Choose a dataset to download."),
#' selectInput("dataset", "Dataset", choices = c("mtcars", "airquality")),
#' downloadButton("downloadData", "Download") #' downloadButton("downloadData", "Download")
#' ) #' )
#' #'
#' server <- function(input, output) { #' server <- function(input, output) {
#' # The requested dataset #' # Our dataset
#' data <- reactive({ #' data <- mtcars
#' get(input$dataset)
#' })
#' #'
#' output$downloadData <- downloadHandler( #' output$downloadData <- downloadHandler(
#' filename = function() { #' filename = function() {
#' # Use the selected dataset as the suggested file name #' paste("data-", Sys.Date(), ".csv", sep="")
#' paste0(input$dataset, ".csv")
#' }, #' },
#' content = function(file) { #' content = function(file) {
#' # Write the dataset to the `file` that will be downloaded #' write.csv(data, file)
#' write.csv(data(), file)
#' } #' }
#' ) #' )
#' } #' }
@@ -1276,29 +1434,23 @@ downloadButton <- function(outputId,
class=NULL, class=NULL,
..., ...,
icon = shiny::icon("download")) { icon = shiny::icon("download")) {
tags$a(id=outputId, aTag <- tags$a(id=outputId,
class='btn btn-default shiny-download-link disabled', class=paste('btn btn-default shiny-download-link', class),
class=class, href='',
href='', target='_blank',
target='_blank', download=NA,
download=NA, validateIcon(icon),
"aria-disabled"="true", label, ...)
tabindex="-1",
validateIcon(icon),
label, ...)
} }
#' @rdname downloadButton #' @rdname downloadButton
#' @export #' @export
downloadLink <- function(outputId, label="Download", class=NULL, ...) { downloadLink <- function(outputId, label="Download", class=NULL, ...) {
tags$a(id=outputId, tags$a(id=outputId,
class='shiny-download-link disabled', class=paste(c('shiny-download-link', class), collapse=" "),
class=class,
href='', href='',
target='_blank', target='_blank',
download=NA, download=NA,
"aria-disabled"="true",
tabindex="-1",
label, ...) label, ...)
} }
@@ -1306,31 +1458,32 @@ downloadLink <- function(outputId, label="Download", class=NULL, ...) {
#' Create an icon #' Create an icon
#' #'
#' Create an icon for use within a page. Icons can appear on their own, inside #' Create an icon for use within a page. Icons can appear on their own, inside
#' of a button, and/or used with [tabPanel()] and [navbarMenu()]. #' of a button, or as an icon for a [tabPanel()] within a
#' [navbarPage()].
#' #'
#' @param name The name of the icon. A name from either [Font #' @param name Name of icon. Icons are drawn from the
#' Awesome](https://fontawesome.com/) (when `lib="font-awesome"`) or #' [Font Awesome Free](https://fontawesome.com/) (currently icons from
#' [Bootstrap #' the v5.13.0 set are supported with the v4 naming convention) and
#' Glyphicons](https://getbootstrap.com/docs/3.3/components/#glyphicons) (when #' [Glyphicons](https://getbootstrap.com/components/#glyphicons)
#' `lib="glyphicon"`) may be provided. Note that the `"fa-"` and #' libraries. Note that the "fa-" and "glyphicon-" prefixes should not be used
#' `"glyphicon-"` prefixes should not appear in name (i.e., the #' in icon names (i.e. the "fa-calendar" icon should be referred to as
#' `"fa-calendar"` icon should be referred to as `"calendar"`). A `name` of #' "calendar")
#' `NULL` may also be provided to get a raw `<i>` tag with no library attached #' @param class Additional classes to customize the style of the icon (see the
#' to it.
#' @param class Additional classes to customize the style of an icon (see the
#' [usage examples](https://fontawesome.com/how-to-use) for details on #' [usage examples](https://fontawesome.com/how-to-use) for details on
#' supported styles). #' supported styles).
#' @param lib The icon library to use. Either `"font-awesome"` or `"glyphicon"`. #' @param lib Icon library to use ("font-awesome" or "glyphicon")
#' @param ... Arguments passed to the `<i>` tag of [htmltools::tags]. #' @param ... Arguments passed to the `<i>` tag of [htmltools::tags]
#' #'
#' @return An `<i>` (icon) HTML tag. #' @return An icon element
#'
#' @seealso For lists of available icons, see
#' [https://fontawesome.com/icons](https://fontawesome.com/icons) and
#' [https://getbootstrap.com/components/#glyphicons](https://getbootstrap.com/components/#glyphicons).
#' #'
#' @seealso For lists of available icons, see <https://fontawesome.com/icons>
#' and <https://getbootstrap.com/docs/3.3/components/#glyphicons>
#' #'
#' @examples #' @examples
#' # add an icon to a submit button #' # add an icon to a submit button
#' submitButton("Update View", icon = icon("redo")) #' submitButton("Update View", icon = icon("refresh"))
#' #'
#' navbarPage("App Title", #' navbarPage("App Title",
#' tabPanel("Plot", icon = icon("bar-chart-o")), #' tabPanel("Plot", icon = icon("bar-chart-o")),
@@ -1339,26 +1492,48 @@ downloadLink <- function(outputId, label="Download", class=NULL, ...) {
#' ) #' )
#' @export #' @export
icon <- function(name, class = NULL, lib = "font-awesome", ...) { icon <- function(name, class = NULL, lib = "font-awesome", ...) {
prefixes <- list(
"font-awesome" = "fa",
"glyphicon" = "glyphicon"
)
prefix <- prefixes[[lib]]
# A NULL name allows for a generic <i> not tied to any library # determine stylesheet
if (is.null(name)) { if (is.null(prefix)) {
lib <- "none" stop("Unknown font library '", lib, "' specified. Must be one of ",
paste0('"', names(prefixes), '"', collapse = ", "))
} }
switch( # build the icon class (allow name to be null so that other functions
lib %||% "", # e.g. buildTabset can pass an explicit class value)
"none" = iconTag(name, class = class, ...), iconClass <- ""
"font-awesome" = fontawesome::fa_i(name = name, class = class, ...), if (!is.null(name)) {
"glyphicon" = iconTag( prefix_class <- prefix
name, class = "glyphicon", class = paste0("glyphicon-", name), if (prefix_class == "fa" && name %in% font_awesome_brands) {
class = class, ... prefix_class <- "fab"
), }
stop("Unknown icon library: ", lib, ". See `?icon` for supported libraries.") iconClass <- paste0(prefix_class, " ", prefix, "-", name)
) }
if (!is.null(class))
iconClass <- paste(iconClass, class)
iconTag <- tags$i(class = iconClass, role = "presentation", `aria-label` = paste(name, "icon"), ...)
# font-awesome needs an additional dependency (glyphicon is in bootstrap)
if (lib == "font-awesome") {
htmlDependencies(iconTag) <- htmlDependency(
"font-awesome", "5.13.0", "www/shared/fontawesome", package = "shiny",
stylesheet = c(
"css/all.min.css",
"css/v4-shims.min.css"
)
)
}
htmltools::browsable(iconTag)
} }
iconTag <- function(name, ...) { # Helper funtion to extract the class from an icon
htmltools::browsable( iconClass <- function(icon) {
tags$i(..., role = "presentation", `aria-label` = paste(name, "icon")) if (!is.null(icon)) icon$attribs$class
)
} }

View File

@@ -1,4 +0,0 @@
# Generated by tools/updateSpinnerTypes.R: do not edit by hand
.busySpinnerTypes <-
c("ring", "ring2", "ring3", "bars", "bars2", "bars3", "pulse",
"pulse2", "pulse3", "dots", "dots2", "dots3")

View File

@@ -1,294 +0,0 @@
#' Enable/disable busy indication
#'
#' Busy indicators provide a visual cue to users when the server is busy
#' calculating outputs or otherwise performing tasks (e.g., producing
#' downloads). When enabled, a spinner is shown on each
#' calculating/recalculating output, and a pulsing banner is shown at the top of
#' the page when the app is otherwise busy. Busy indication is enabled by
#' default for UI created with \pkg{bslib}, but must be enabled otherwise. To
#' enable/disable, include the result of this function in anywhere in the app's
#' UI.
#'
#' When both `spinners` and `pulse` are set to `TRUE`, the pulse is
#' automatically disabled when spinner(s) are active. When both `spinners` and
#' `pulse` are set to `FALSE`, no busy indication is shown (other than the
#' graying out of recalculating outputs).
#'
#' @param ... Currently ignored.
#' @param spinners Whether to show a spinner on each calculating/recalculating
#' output.
#' @param pulse Whether to show a pulsing banner at the top of the page when the
#' app is busy.
#' @param fade Whether to fade recalculating outputs. A value of `FALSE` is
#' equivalent to `busyIndicatorOptions(fade_opacity=1)`.
#'
#' @export
#' @seealso [busyIndicatorOptions()] for customizing the appearance of the busy
#' indicators.
#' @examplesIf rlang::is_interactive()
#'
#' library(bslib)
#'
#' ui <- page_fillable(
#' useBusyIndicators(),
#' card(
#' card_header(
#' "A plot",
#' input_task_button("simulate", "Simulate"),
#' class = "d-flex justify-content-between align-items-center"
#' ),
#' plotOutput("p"),
#' )
#' )
#'
#' server <- function(input, output) {
#' output$p <- renderPlot({
#' input$simulate
#' Sys.sleep(4)
#' plot(x = rnorm(100), y = rnorm(100))
#' })
#' }
#'
#' shinyApp(ui, server)
useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE, fade = TRUE) {
rlang::check_dots_empty()
attrs <- list("shinyBusySpinners" = spinners, "shinyBusyPulse" = pulse)
js <- vapply(names(attrs), character(1), FUN = function(key) {
if (attrs[[key]]) {
sprintf("document.documentElement.dataset.%s = 'true';", key)
} else {
sprintf("delete document.documentElement.dataset.%s;", key)
}
})
# TODO: it'd be nice if htmltools had something like a page_attrs() that allowed us
# to do this without needing to inject JS into the head.
res <- tags$script(HTML(paste(js, collapse = "\n")))
if (!fade) {
res <- tagList(res, fadeOptions(opacity = 1))
}
res
}
#' Customize busy indicator options
#'
#' @description
#' Shiny automatically includes busy indicators, which more specifically means:
#' 1. Calculating/recalculating outputs have a spinner overlay.
#' 2. Outputs fade out/in when recalculating.
#' 3. When no outputs are calculating/recalculating, but Shiny is busy
#' doing something else (e.g., a download, side-effect, etc), a page-level
#' pulsing banner is shown.
#'
#' This function allows you to customize the appearance of these busy indicators
#' by including the result of this function inside the app's UI. Note that,
#' unless `spinner_selector` (or `fade_selector`) is specified, the spinner/fade
#' customization applies to the parent element. If the customization should
#' instead apply to the entire page, set `spinner_selector = 'html'` and
#' `fade_selector = 'html'`.
#'
#' @param ... Currently ignored.
#' @param spinner_type The type of spinner. Pre-bundled types include:
#' '`r paste0(.busySpinnerTypes, collapse = "', '")`'.
#'
#' A path to a local SVG file can also be provided. The SVG should adhere to
#' the following rules:
#' * The SVG itself should contain the animation.
#' * It should avoid absolute sizes (the spinner's containing DOM element
#' size is set in CSS by `spinner_size`, so it should fill that container).
#' * It should avoid setting absolute colors (the spinner's containing DOM element
#' color is set in CSS by `spinner_color`, so it should inherit that color).
#' @param spinner_color The color of the spinner. This can be any valid CSS
#' color. Defaults to the app's "primary" color if Bootstrap is on the page.
#' @param spinner_size The size of the spinner. This can be any valid CSS size.
#' @param spinner_delay The amount of time to wait before showing the spinner.
#' This can be any valid CSS time and can be useful for not showing the spinner
#' if the computation finishes quickly.
#' @param spinner_selector A character string containing a CSS selector for
#' scoping the spinner customization. The default (`NULL`) will apply the
#' spinner customization to the parent element of the spinner.
#' @param fade_opacity The opacity (a number between 0 and 1) for recalculating
#' output. Set to 1 to "disable" the fade.
#' @param fade_selector A character string containing a CSS selector for
#' scoping the spinner customization. The default (`NULL`) will apply the
#' spinner customization to the parent element of the spinner.
#' @param pulse_background A CSS background definition for the pulse. The
#' default uses a
#' [linear-gradient](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient)
#' of the theme's indigo, purple, and pink colors.
#' @param pulse_height The height of the pulsing banner. This can be any valid
#' CSS size.
#' @param pulse_speed The speed of the pulsing banner. This can be any valid CSS
#' time.
#'
#' @export
#' @seealso [useBusyIndicators()] to disable/enable busy indicators.
#' @examplesIf rlang::is_interactive()
#'
#' library(bslib)
#'
#' card_ui <- function(id, spinner_type = id) {
#' card(
#' busyIndicatorOptions(spinner_type = spinner_type),
#' card_header(paste("Spinner:", spinner_type)),
#' plotOutput(shiny::NS(id, "plot"))
#' )
#' }
#'
#' card_server <- function(id, simulate = reactive()) {
#' moduleServer(
#' id = id,
#' function(input, output, session) {
#' output$plot <- renderPlot({
#' Sys.sleep(1)
#' simulate()
#' plot(x = rnorm(100), y = rnorm(100))
#' })
#' }
#' )
#' }
#'
#' ui <- page_fillable(
#' useBusyIndicators(),
#' input_task_button("simulate", "Simulate", icon = icon("refresh")),
#' layout_columns(
#' card_ui("ring"),
#' card_ui("bars"),
#' card_ui("dots"),
#' card_ui("pulse"),
#' col_widths = 6
#' )
#' )
#'
#' server <- function(input, output, session) {
#' simulate <- reactive(input$simulate)
#' card_server("ring", simulate)
#' card_server("bars", simulate)
#' card_server("dots", simulate)
#' card_server("pulse", simulate)
#' }
#'
#' shinyApp(ui, server)
#'
busyIndicatorOptions <- function(
...,
spinner_type = NULL,
spinner_color = NULL,
spinner_size = NULL,
spinner_delay = NULL,
spinner_selector = NULL,
fade_opacity = NULL,
fade_selector = NULL,
pulse_background = NULL,
pulse_height = NULL,
pulse_speed = NULL
) {
rlang::check_dots_empty()
res <- tagList(
spinnerOptions(
type = spinner_type,
color = spinner_color,
size = spinner_size,
delay = spinner_delay,
selector = spinner_selector
),
fadeOptions(opacity = fade_opacity, selector = fade_selector),
pulseOptions(
background = pulse_background,
height = pulse_height,
speed = pulse_speed
)
)
bslib::as.card_item(dropNulls(res))
}
spinnerOptions <- function(type = NULL, color = NULL, size = NULL, delay = NULL, selector = NULL) {
if (is.null(type) && is.null(color) && is.null(size) && is.null(delay) && is.null(selector)) {
return(NULL)
}
url <- NULL
if (!is.null(type)) {
stopifnot(is.character(type) && length(type) == 1)
if (file.exists(type) && grepl("\\.svg$", type)) {
typeRaw <- readBin(type, "raw", n = file.info(type)$size)
url <- sprintf("url('data:image/svg+xml;base64,%s')", rawToBase64(typeRaw))
} else {
type <- rlang::arg_match(type, .busySpinnerTypes)
url <- sprintf("url('spinners/%s.svg')", type)
}
}
# Options controlled via CSS variables.
css_vars <- htmltools::css(
"--shiny-spinner-url" = url,
"--shiny-spinner-color" = htmltools::parseCssColors(color),
"--shiny-spinner-size" = htmltools::validateCssUnit(size),
"--shiny-spinner-delay" = delay
)
id <- NULL
if (is.null(selector)) {
id <- paste0("spinner-options-", p_randomInt(100, 1000000))
selector <- sprintf(":has(> #%s)", id)
}
css <- HTML(paste0(selector, " {", css_vars, "}"))
tags$style(css, id = id)
}
fadeOptions <- function(opacity = NULL, selector = NULL) {
if (is.null(opacity) && is.null(selector)) {
return(NULL)
}
css_vars <- htmltools::css(
"--shiny-fade-opacity" = opacity
)
id <- NULL
if (is.null(selector)) {
id <- paste0("fade-options-", p_randomInt(100, 1000000))
selector <- sprintf(":has(> #%s)", id)
}
css <- HTML(paste0(selector, " {", css_vars, "}"))
tags$style(css, id = id)
}
pulseOptions <- function(background = NULL, height = NULL, speed = NULL) {
if (is.null(background) && is.null(height) && is.null(speed)) {
return(NULL)
}
css_vars <- htmltools::css(
"--shiny-pulse-background" = background,
"--shiny-pulse-height" = htmltools::validateCssUnit(height),
"--shiny-pulse-speed" = speed
)
tags$style(HTML(paste0(":root {", css_vars, "}")))
}
busyIndicatorDependency <- function() {
htmlDependency(
name = "shiny-busy-indicators",
version = get_package_version("shiny"),
src = "www/shared/busy-indicators",
package = "shiny",
stylesheet = "busy-indicators.css",
# TODO-future: In next release make spinners and pulse opt-out
# head = as.character(useBusyIndicators())
)
}

View File

@@ -75,18 +75,6 @@ getCallNames <- function(calls) {
}) })
} }
# A stripped down version of getCallNames() that intentionally avoids deparsing expressions.
# Instead, it leaves expressions to be directly `rlang::hash()` (for de-duplication), which
# is much faster than deparsing then hashing.
getCallNamesForHash <- function(calls) {
lapply(calls, function(call) {
name <- call[[1L]]
if (is.function(name)) return("<Anonymous>")
if (typeof(name) == "promise") return("<Promise>")
name
})
}
getLocs <- function(calls) { getLocs <- function(calls) {
vapply(calls, function(call) { vapply(calls, function(call) {
srcref <- attr(call, "srcref", exact = TRUE) srcref <- attr(call, "srcref", exact = TRUE)
@@ -142,44 +130,6 @@ captureStackTraces <- function(expr) {
#' @include globals.R #' @include globals.R
.globals$deepStack <- NULL .globals$deepStack <- NULL
getCallStackDigest <- function(callStack, warn = FALSE) {
dg <- attr(callStack, "shiny.stack.digest", exact = TRUE)
if (!is.null(dg)) {
return(dg)
}
if (isTRUE(warn)) {
rlang::warn(
"Call stack doesn't have a cached digest; expensively computing one now",
.frequency = "once",
.frequency_id = "deepstack-uncached-digest-warning"
)
}
rlang::hash(getCallNamesForHash(callStack))
}
saveCallStackDigest <- function(callStack) {
attr(callStack, "shiny.stack.digest") <- getCallStackDigest(callStack, warn = FALSE)
callStack
}
# Appends a call stack to a list of call stacks, but only if it's not already
# in the list. The list is deduplicated by digest; ideally the digests on the
# list are cached before calling this function (you will get a warning if not).
appendCallStackWithDedupe <- function(lst, x) {
digests <- vapply(lst, getCallStackDigest, character(1), warn = TRUE)
xdigest <- getCallStackDigest(x, warn = TRUE)
stopifnot(all(nzchar(digests)))
stopifnot(length(xdigest) == 1)
stopifnot(nzchar(xdigest))
if (xdigest %in% digests) {
return(lst)
} else {
return(c(lst, list(x)))
}
}
createStackTracePromiseDomain <- function() { createStackTracePromiseDomain <- function() {
# These are actually stateless, we wouldn't have to create a new one each time # These are actually stateless, we wouldn't have to create a new one each time
# if we didn't want to. They're pretty cheap though. # if we didn't want to. They're pretty cheap though.
@@ -192,14 +142,13 @@ createStackTracePromiseDomain <- function() {
currentStack <- sys.calls() currentStack <- sys.calls()
currentParents <- sys.parents() currentParents <- sys.parents()
attr(currentStack, "parents") <- currentParents attr(currentStack, "parents") <- currentParents
currentStack <- saveCallStackDigest(currentStack)
currentDeepStack <- .globals$deepStack currentDeepStack <- .globals$deepStack
} }
function(...) { function(...) {
# Fulfill time # Fulfill time
if (deepStacksEnabled()) { if (deepStacksEnabled()) {
origDeepStack <- .globals$deepStack origDeepStack <- .globals$deepStack
.globals$deepStack <- appendCallStackWithDedupe(currentDeepStack, currentStack) .globals$deepStack <- c(currentDeepStack, list(currentStack))
on.exit(.globals$deepStack <- origDeepStack, add = TRUE) on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
} }
@@ -216,14 +165,13 @@ createStackTracePromiseDomain <- function() {
currentStack <- sys.calls() currentStack <- sys.calls()
currentParents <- sys.parents() currentParents <- sys.parents()
attr(currentStack, "parents") <- currentParents attr(currentStack, "parents") <- currentParents
currentStack <- saveCallStackDigest(currentStack)
currentDeepStack <- .globals$deepStack currentDeepStack <- .globals$deepStack
} }
function(...) { function(...) {
# Fulfill time # Fulfill time
if (deepStacksEnabled()) { if (deepStacksEnabled()) {
origDeepStack <- .globals$deepStack origDeepStack <- .globals$deepStack
.globals$deepStack <- appendCallStackWithDedupe(currentDeepStack, currentStack) .globals$deepStack <- c(currentDeepStack, list(currentStack))
on.exit(.globals$deepStack <- origDeepStack, add = TRUE) on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
} }
@@ -251,7 +199,6 @@ doCaptureStack <- function(e) {
calls <- sys.calls() calls <- sys.calls()
parents <- sys.parents() parents <- sys.parents()
attr(calls, "parents") <- parents attr(calls, "parents") <- parents
calls <- saveCallStackDigest(calls)
attr(e, "stack.trace") <- calls attr(e, "stack.trace") <- calls
} }
if (deepStacksEnabled()) { if (deepStacksEnabled()) {
@@ -281,9 +228,7 @@ withLogErrors <- function(expr,
if (promises::is.promise(result)) { if (promises::is.promise(result)) {
result <- promises::catch(result, function(cond) { result <- promises::catch(result, function(cond) {
# Don't print shiny.silent.error (i.e. validation errors) # Don't print shiny.silent.error (i.e. validation errors)
if (cnd_inherits(cond, "shiny.silent.error")) { if (inherits(cond, "shiny.silent.error")) return()
return()
}
if (isTRUE(getOption("show.error.messages"))) { if (isTRUE(getOption("show.error.messages"))) {
printError(cond, full = full, offset = offset) printError(cond, full = full, offset = offset)
} }
@@ -294,7 +239,7 @@ withLogErrors <- function(expr,
}, },
error = function(cond) { error = function(cond) {
# Don't print shiny.silent.error (i.e. validation errors) # Don't print shiny.silent.error (i.e. validation errors)
if (cnd_inherits(cond, "shiny.silent.error")) return() if (inherits(cond, "shiny.silent.error")) return()
if (isTRUE(getOption("show.error.messages"))) { if (isTRUE(getOption("show.error.messages"))) {
printError(cond, full = full, offset = offset) printError(cond, full = full, offset = offset)
} }
@@ -334,113 +279,162 @@ printStackTrace <- function(cond,
full = get_devmode_option("shiny.fullstacktrace", FALSE), full = get_devmode_option("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) { offset = getOption("shiny.stacktraceoffset", TRUE)) {
stackTraces <- c(
attr(cond, "deep.stack.trace", exact = TRUE),
list(attr(cond, "stack.trace", exact = TRUE))
)
# Stripping of stack traces is the one step where the different stack traces
# interact. So we need to do this in one go, instead of individually within
# printOneStackTrace.
if (!full) {
stripResults <- stripStackTraces(lapply(stackTraces, getCallNames))
} else {
# If full is TRUE, we don't want to strip anything
stripResults <- rep_len(list(TRUE), length(stackTraces))
}
mapply(
seq_along(stackTraces),
rev(stackTraces),
rev(stripResults),
FUN = function(i, trace, stripResult) {
if (is.integer(trace)) {
noun <- if (trace > 1L) "traces" else "trace"
message("[ reached getOption(\"shiny.deepstacktrace\") -- omitted ", trace, " more stack ", noun, " ]")
} else {
if (i != 1) {
message("From earlier call:")
}
printOneStackTrace(
stackTrace = trace,
stripResult = stripResult,
full = full,
offset = offset
)
}
# No mapply return value--we're just printing
NULL
},
SIMPLIFY = FALSE
)
invisible()
}
printOneStackTrace <- function(stackTrace, stripResult, full, offset) {
calls <- offsetSrcrefs(stackTrace, offset = offset)
callNames <- getCallNames(stackTrace)
parents <- attr(stackTrace, "parents", exact = TRUE)
should_drop <- !full should_drop <- !full
should_strip <- !full should_strip <- !full
should_prune <- !full should_prune <- !full
if (should_drop) { stackTraceCalls <- c(
toKeep <- dropTrivialFrames(callNames) attr(cond, "deep.stack.trace", exact = TRUE),
calls <- calls[toKeep] list(attr(cond, "stack.trace", exact = TRUE))
callNames <- callNames[toKeep]
parents <- parents[toKeep]
stripResult <- stripResult[toKeep]
}
toShow <- rep(TRUE, length(callNames))
if (should_prune) {
toShow <- toShow & pruneStackTrace(parents)
}
if (should_strip) {
toShow <- toShow & stripResult
}
# If we're running in testthat, hide the parts of the stack trace that can
# vary based on how testthat was launched. It's critical that this is not
# happen at the same time as dropTrivialFrames, which happens before
# pruneStackTrace; because dropTrivialTestFrames removes calls from the top
# (or bottom? whichever is the oldest?) of the stack, it breaks `parents`
# which is based on absolute indices of calls. dropTrivialFrames gets away
# with this because it only removes calls from the opposite side of the stack.
toShow <- toShow & dropTrivialTestFrames(callNames)
st <- data.frame(
num = rev(which(toShow)),
call = rev(callNames[toShow]),
loc = rev(getLocs(calls[toShow])),
category = rev(getCallCategories(calls[toShow])),
stringsAsFactors = FALSE
) )
if (nrow(st) == 0) { stackTraceParents <- lapply(stackTraceCalls, attr, which = "parents", exact = TRUE)
message(" [No stack trace available]") stackTraceCallNames <- lapply(stackTraceCalls, getCallNames)
} else { stackTraceCalls <- lapply(stackTraceCalls, offsetSrcrefs, offset = offset)
width <- floor(log10(max(st$num))) + 1
formatted <- paste0( # Use dropTrivialFrames logic to remove trailing bits (.handleSimpleError, h)
" ", if (should_drop) {
formatC(st$num, width = width), # toKeep is a list of logical vectors, of which elements (stack frames) to keep
": ", toKeep <- lapply(stackTraceCallNames, dropTrivialFrames)
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) { # We apply the list of logical vector indices to each data structure
if (category == "pkg") stackTraceCalls <- mapply(stackTraceCalls, FUN = `[`, toKeep, SIMPLIFY = FALSE)
cli::col_silver(name) stackTraceCallNames <- mapply(stackTraceCallNames, FUN = `[`, toKeep, SIMPLIFY = FALSE)
else if (category == "user") stackTraceParents <- mapply(stackTraceParents, FUN = `[`, toKeep, SIMPLIFY = FALSE)
cli::style_bold(cli::col_blue(name))
else
cli::col_white(name)
}),
"\n"
)
cat(file = stderr(), formatted, sep = "")
} }
invisible(st) delayedAssign("all_true", {
# List of logical vectors that are all TRUE, the same shape as
# stackTraceCallNames. Delay the evaluation so we don't create it unless
# we need it, but if we need it twice then we don't pay to create it twice.
lapply(stackTraceCallNames, function(st) {
rep_len(TRUE, length(st))
})
})
# stripStackTraces and lapply(stackTraceParents, pruneStackTrace) return lists
# of logical vectors. Use mapply(FUN = `&`) to boolean-and each pair of the
# logical vectors.
toShow <- mapply(
if (should_strip) stripStackTraces(stackTraceCallNames) else all_true,
if (should_prune) lapply(stackTraceParents, pruneStackTrace) else all_true,
FUN = `&`,
SIMPLIFY = FALSE
)
dfs <- mapply(seq_along(stackTraceCalls), rev(stackTraceCalls), rev(stackTraceCallNames), rev(toShow), FUN = function(i, calls, nms, index) {
st <- data.frame(
num = rev(which(index)),
call = rev(nms[index]),
loc = rev(getLocs(calls[index])),
category = rev(getCallCategories(calls[index])),
stringsAsFactors = FALSE
)
if (i != 1) {
message("From earlier call:")
}
if (nrow(st) == 0) {
message(" [No stack trace available]")
} else {
width <- floor(log10(max(st$num))) + 1
formatted <- paste0(
" ",
formatC(st$num, width = width),
": ",
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
if (category == "pkg")
crayon::silver(name)
else if (category == "user")
crayon::blue$bold(name)
else
crayon::white(name)
}),
"\n"
)
cat(file = stderr(), formatted, sep = "")
}
st
}, SIMPLIFY = FALSE)
invisible()
}
#' @details `extractStackTrace` takes a list of calls (e.g. as returned
#' from `conditionStackTrace(cond)`) and returns a data frame with one
#' row for each stack frame and the columns `num` (stack frame number),
#' `call` (a function name or similar), and `loc` (source file path
#' and line number, if available). It was deprecated after shiny 1.0.5 because
#' it doesn't support deep stack traces.
#' @rdname stacktrace
#' @export
extractStackTrace <- function(calls,
full = get_devmode_option("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
shinyDeprecated(
"1.0.5", "extractStackTrace()",
details = "Please contact the Shiny team if you were using this functionality."
)
srcrefs <- getSrcRefs(calls)
if (offset) {
# Offset calls vs. srcrefs by 1 to make them more intuitive.
# E.g. for "foo [bar.R:10]", line 10 of bar.R will be part of
# the definition of foo().
srcrefs <- c(utils::tail(srcrefs, -1), list(NULL))
}
calls <- setSrcRefs(calls, srcrefs)
callnames <- getCallNames(calls)
# Hide and show parts of the callstack based on ..stacktrace(on|off)..
if (full) {
toShow <- rep.int(TRUE, length(calls))
} else {
# Remove stop(), .handleSimpleError(), and h() calls from the end of
# the calls--they don't add any helpful information. But only remove
# the last *contiguous* block of them, and then, only if they are the
# last thing in the calls list.
hideable <- callnames %in% c("stop", ".handleSimpleError", "h")
# What's the last that *didn't* match stop/.handleSimpleError/h?
lastGoodCall <- max(which(!hideable))
toRemove <- length(calls) - lastGoodCall
# But don't remove more than 5 levels--that's an indication we might
# have gotten it wrong, I guess
if (toRemove > 0 && toRemove < 5) {
calls <- utils::head(calls, -toRemove)
callnames <- utils::head(callnames, -toRemove)
}
# This uses a ref-counting scheme. It might make sense to switch this
# to a toggling scheme, so the most recent ..stacktrace(on|off)..
# directive wins, regardless of what came before it.
# Also explicitly remove ..stacktraceon.. because it can appear with
# score > 0 but still should never be shown.
score <- rep.int(0, length(callnames))
score[callnames == "..stacktraceoff.."] <- -1
score[callnames == "..stacktraceon.."] <- 1
toShow <- (1 + cumsum(score)) > 0 & !(callnames %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
# doTryCatch, tryCatchOne, and tryCatchList are not informative--they're
# just internals for tryCatch
toShow <- toShow & !(callnames %in% c("doTryCatch", "tryCatchOne", "tryCatchList"))
}
calls <- calls[toShow]
calls <- rev(calls) # Show in traceback() order
index <- rev(which(toShow))
width <- floor(log10(max(index))) + 1
data.frame(
num = index,
call = getCallNames(calls),
loc = getLocs(calls),
category = getCallCategories(calls),
stringsAsFactors = FALSE
)
} }
stripStackTraces <- function(stackTraces, values = FALSE) { stripStackTraces <- function(stackTraces, values = FALSE) {
@@ -501,17 +495,8 @@ pruneStackTrace <- function(parents) {
# Loop over the parent indices. Anything that is not parented by current_node # Loop over the parent indices. Anything that is not parented by current_node
# (a.k.a. last-known-good node), or is a dupe, can be discarded. Anything that # (a.k.a. last-known-good node), or is a dupe, can be discarded. Anything that
# is kept becomes the new current_node. # is kept becomes the new current_node.
#
# jcheng 2022-03-18: Two more reasons a node can be kept:
# 1. parent is 0
# 2. parent is i
# Not sure why either of these situations happen, but they're common when
# interacting with rlang/dplyr errors. See issue rstudio/shiny#3250 for repro
# cases.
include <- vapply(seq_along(parents), function(i) { include <- vapply(seq_along(parents), function(i) {
if ((!is_dupe[[i]] && parents[[i]] == current_node) || if (!is_dupe[[i]] && parents[[i]] == current_node) {
parents[[i]] == 0 ||
parents[[i]] == i) {
current_node <<- i current_node <<- i
TRUE TRUE
} else { } else {
@@ -538,33 +523,6 @@ dropTrivialFrames <- function(callnames) {
) )
} }
dropTrivialTestFrames <- function(callnames) {
if (!identical(Sys.getenv("TESTTHAT_IS_SNAPSHOT"), "true")) {
return(rep_len(TRUE, length(callnames)))
}
hideable <- callnames %in% c(
"test",
"devtools::test",
"test_check",
"testthat::test_check",
"test_dir",
"testthat::test_dir",
"test_file",
"testthat::test_file",
"test_local",
"testthat::test_local"
)
firstGoodCall <- min(which(!hideable))
toRemove <- firstGoodCall - 1L
c(
rep_len(FALSE, toRemove),
rep_len(TRUE, length(callnames) - toRemove)
)
}
offsetSrcrefs <- function(calls, offset = TRUE) { offsetSrcrefs <- function(calls, offset = TRUE) {
if (offset) { if (offset) {
srcrefs <- getSrcRefs(calls) srcrefs <- getSrcRefs(calls)
@@ -573,12 +531,49 @@ offsetSrcrefs <- function(calls, offset = TRUE) {
# E.g. for "foo [bar.R:10]", line 10 of bar.R will be part of # E.g. for "foo [bar.R:10]", line 10 of bar.R will be part of
# the definition of foo(). # the definition of foo().
srcrefs <- c(utils::tail(srcrefs, -1), list(NULL)) srcrefs <- c(utils::tail(srcrefs, -1), list(NULL))
calls <- setSrcRefs(calls, srcrefs) calls <- setSrcRefs(calls, srcrefs)
} }
calls calls
} }
#' @details `formatStackTrace` is similar to `extractStackTrace`, but
#' it returns a preformatted character vector instead of a data frame. It was
#' deprecated after shiny 1.0.5 because it doesn't support deep stack traces.
#' @param indent A string to prefix every line of the stack trace.
#' @rdname stacktrace
#' @export
formatStackTrace <- function(calls, indent = " ",
full = get_devmode_option("shiny.fullstacktrace", FALSE),
offset = getOption("shiny.stacktraceoffset", TRUE)) {
shinyDeprecated(
"1.0.5", "formatStackTrace()",
details = "Please contact the Shiny team if you were using this functionality."
)
st <- extractStackTrace(calls, full = full, offset = offset)
if (nrow(st) == 0) {
return(character(0))
}
width <- floor(log10(max(st$num))) + 1
paste0(
indent,
formatC(st$num, width = width),
": ",
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
if (category == "pkg")
crayon::silver(name)
else if (category == "user")
crayon::blue$bold(name)
else
crayon::white(name)
})
)
}
getSrcRefs <- function(calls) { getSrcRefs <- function(calls) {
lapply(calls, function(call) { lapply(calls, function(call) {
attr(call, "srcref", exact = TRUE) attr(call, "srcref", exact = TRUE)

View File

@@ -9,19 +9,13 @@
#' @param details Additional information to be added after a new line to the displayed message #' @param details Additional information to be added after a new line to the displayed message
#' @keywords internal #' @keywords internal
shinyDeprecated <- function( shinyDeprecated <- function(
version, version, what, with = NULL, details = NULL
what,
with = NULL,
details = NULL,
type = c("deprecated", "superseded")
) { ) {
if (is_false(getOption("shiny.deprecation.messages"))) { if (is_false(getOption("shiny.deprecation.messages"))) {
return(invisible()) return(invisible())
} }
type <- match.arg(type) msg <- paste0("`", what, "` is deprecated as of shiny ", version, ".")
msg <- paste0("`", what, "` is ", type, " as of shiny ", version, ".")
if (!is.null(with)) { if (!is.null(with)) {
msg <- paste0(msg, "\n", "Please use `", with, "` instead.") msg <- paste0(msg, "\n", "Please use `", with, "` instead.")
} }
@@ -38,20 +32,13 @@ deprecatedEnvQuotedMessage <- function() {
if (!in_devmode()) return(invisible()) if (!in_devmode()) return(invisible())
if (is_false(getOption("shiny.deprecation.messages"))) return(invisible()) if (is_false(getOption("shiny.deprecation.messages"))) return(invisible())
# Capture calling function # manually
grandparent_call <- sys.call(-2)
# Turn language into user friendly string
grandparent_txt <- paste0(utils::capture.output({grandparent_call}), collapse = "\n")
msg <- paste0( msg <- paste0(
"The `env` and `quoted` arguments are deprecated as of shiny 1.7.0.", "The `env` and `quoted` arguments are deprecated as of shiny 1.6.0.",
" Please use quosures from `rlang` instead.\n", " Please use quosures from `rlang` instead.\n",
"See <https://github.com/rstudio/shiny/issues/3108> for more information.\n", "See <https://github.com/rstudio/shiny/issues/3108> for more information."
"Function call:\n",
grandparent_txt
) )
# Call less often as users do not have much control over this warning rlang::inform(message = msg, .frequency = "always", .frequency_id = msg, .file = stderr())
rlang::inform(message = msg, .frequency = "regularly", .frequency_id = msg, .file = stderr())
} }
@@ -73,7 +60,7 @@ diskCache <- function(
logfile = NULL logfile = NULL
) { ) {
shinyDeprecated("1.6.0", "diskCache()", "cachem::cache_disk()") shinyDeprecated("1.6.0", "diskCache()", "cachem::cache_disk()")
if (is_present(exec_missing)) { if (lifecycle::is_present(exec_missing)) {
shinyDeprecated("1.6.0", "diskCache(exec_missing =)") shinyDeprecated("1.6.0", "diskCache(exec_missing =)")
} }
@@ -106,7 +93,7 @@ memoryCache <- function(
logfile = NULL) logfile = NULL)
{ {
shinyDeprecated("1.6.0", "diskCache()", "cachem::cache_mem()") shinyDeprecated("1.6.0", "diskCache()", "cachem::cache_mem()")
if (is_present(exec_missing)) { if (lifecycle::is_present(exec_missing)) {
shinyDeprecated("1.6.0", "diskCache(exec_missing =)") shinyDeprecated("1.6.0", "diskCache(exec_missing =)")
} }

View File

@@ -1,6 +1,6 @@
#' Shiny Developer Mode #' Shiny Developer Mode
#' #'
#' @description `r lifecycle::badge("experimental")` #' @description \lifecycle{experimental}
#' #'
#' Developer Mode enables a number of [options()] to make a developer's life #' Developer Mode enables a number of [options()] to make a developer's life
#' easier, like enabling non-minified JS and printing messages about #' easier, like enabling non-minified JS and printing messages about
@@ -128,12 +128,6 @@ in_devmode <- function() {
!identical(Sys.getenv("TESTTHAT"), "true") !identical(Sys.getenv("TESTTHAT"), "true")
} }
in_client_devmode <- function() {
# Client-side devmode enables client-side only dev features without local
# devmode. Currently, the main feature is the client-side error console.
isTRUE(getOption("shiny.client_devmode", FALSE))
}
#' @describeIn devmode Temporarily set Shiny Developer Mode and Developer #' @describeIn devmode Temporarily set Shiny Developer Mode and Developer
#' message verbosity #' message verbosity
#' @param code Code to execute with the temporary Dev Mode options set #' @param code Code to execute with the temporary Dev Mode options set
@@ -196,10 +190,8 @@ devmode_inform <- function(
registered_devmode_options <- NULL #' @include map.R
on_load({ registered_devmode_options <- Map$new()
registered_devmode_options <- Map$new()
})
#' @describeIn devmode Registers a Shiny Developer Mode option with an updated #' @describeIn devmode Registers a Shiny Developer Mode option with an updated
#' value and Developer message. This registration method allows package #' value and Developer message. This registration method allows package
@@ -241,7 +233,6 @@ on_load({
#' devmode_default = FALSE #' devmode_default = FALSE
#' ) #' )
#' ``` #' ```
#'
#' @param name Name of option to look for in `options()` #' @param name Name of option to look for in `options()`
#' @param default Default value to return if `in_devmode()` returns #' @param default Default value to return if `in_devmode()` returns
#' `TRUE` and the specified option is not set in [`options()`]. #' `TRUE` and the specified option is not set in [`options()`].
@@ -252,7 +243,6 @@ on_load({
#' `TRUE` and the specified option is not set in [`options()`]. For #' `TRUE` and the specified option is not set in [`options()`]. For
#' `get_devmode_option()`, if `devmode_default` is missing, the #' `get_devmode_option()`, if `devmode_default` is missing, the
#' registered `devmode_default` value will be used. #' registered `devmode_default` value will be used.
#' @export
#' @examples #' @examples
#' # Ex: Within shiny, we register the option "shiny.minified" #' # Ex: Within shiny, we register the option "shiny.minified"
#' # to default to `FALSE` when in Dev Mode #' # to default to `FALSE` when in Dev Mode
@@ -348,22 +338,21 @@ get_devmode_option <- function(
} }
on_load({
register_devmode_option(
"shiny.autoreload",
"Turning on shiny autoreload. To disable, call `options(shiny.autoreload = FALSE)`",
TRUE
)
register_devmode_option( register_devmode_option(
"shiny.minified", "shiny.autoreload",
"Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`", "Turning on shiny autoreload. To disable, call `options(shiny.autoreload = FALSE)`",
FALSE TRUE
) )
register_devmode_option( register_devmode_option(
"shiny.fullstacktrace", "shiny.minified",
"Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`", "Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`",
TRUE FALSE
) )
})
register_devmode_option(
"shiny.fullstacktrace",
"Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`",
TRUE
)

View File

@@ -1,255 +0,0 @@
#' Task or computation that proceeds in the background
#'
#' @description In normal Shiny reactive code, whenever an observer, calc, or
#' output is busy computing, it blocks the current session from receiving any
#' inputs or attempting to proceed with any other computation related to that
#' session.
#'
#' The `ExtendedTask` class allows you to have an expensive operation that is
#' started by a reactive effect, and whose (eventual) results can be accessed
#' by a regular observer, calc, or output; but during the course of the
#' operation, the current session is completely unblocked, allowing the user
#' to continue using the rest of the app while the operation proceeds in the
#' background.
#'
#' Note that each `ExtendedTask` object does not represent a _single
#' invocation_ of its long-running function. Rather, it's an object that is
#' used to invoke the function with different arguments, keeps track of
#' whether an invocation is in progress, and provides ways to get at the
#' current status or results of the operation. A single `ExtendedTask` object
#' does not permit overlapping invocations: if the `invoke()` method is called
#' before the previous `invoke()` is completed, the new invocation will not
#' begin until the previous invocation has completed.
#'
#' @section `ExtendedTask` versus asynchronous reactives:
#'
#' Shiny has long supported [using
#' \{promises\}](https://rstudio.github.io/promises/articles/promises_06_shiny.html)
#' to write asynchronous observers, calcs, or outputs. You may be wondering
#' what the differences are between those techniques and this class.
#'
#' Asynchronous observers, calcs, and outputs are not--and have never
#' been--designed to let a user start a long-running operation, while keeping
#' that very same (browser) session responsive to other interactions. Instead,
#' they unblock other sessions, so you can take a long-running operation that
#' would normally bring the entire R process to a halt and limit the blocking
#' to just the session that started the operation. (For more details, see the
#' section on ["The Flush
#' Cycle"](https://rstudio.github.io/promises/articles/promises_06_shiny.html#the-flush-cycle).)
#'
#' `ExtendedTask`, on the other hand, invokes an asynchronous function (that
#' is, a function that quickly returns a promise) and allows even that very
#' session to immediately unblock and carry on with other user interactions.
#'
#' @examplesIf rlang::is_interactive() && rlang::is_installed("mirai")
#' library(shiny)
#' library(bslib)
#' library(mirai)
#'
#' # Set background processes for running tasks
#' daemons(1)
#' # Reset when the app is stopped
#' onStop(function() daemons(0))
#'
#' ui <- page_fluid(
#' titlePanel("Extended Task Demo"),
#' p(
#' 'Click the button below to perform a "calculation"',
#' "that takes a while to perform."
#' ),
#' input_task_button("recalculate", "Recalculate"),
#' p(textOutput("result"))
#' )
#'
#' server <- function(input, output) {
#' rand_task <- ExtendedTask$new(function() {
#' mirai(
#' {
#' # Slow operation goes here
#' Sys.sleep(2)
#' sample(1:100, 1)
#' }
#' )
#' })
#'
#' # Make button state reflect task.
#' # If using R >=4.1, you can do this instead:
#' # rand_task <- ExtendedTask$new(...) |> bind_task_button("recalculate")
#' bind_task_button(rand_task, "recalculate")
#'
#' observeEvent(input$recalculate, {
#' # Invoke the extended in an observer
#' rand_task$invoke()
#' })
#'
#' output$result <- renderText({
#' # React to updated results when the task completes
#' number <- rand_task$result()
#' paste0("Your number is ", number, ".")
#' })
#' }
#'
#' shinyApp(ui, server)
#'
#' @export
ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
public = list(
#' @description
#' Creates a new `ExtendedTask` object. `ExtendedTask` should generally be
#' created either at the top of a server function, or at the top of a module
#' server function.
#'
#' @param func The long-running operation to execute. This should be an
#' asynchronous function, meaning, it should use the
#' [\{promises\}](https://rstudio.github.io/promises/) package, most
#' likely in conjunction with the
#' [\{mirai\}](https://mirai.r-lib.org) or
#' [\{future\}](https://rstudio.github.io/promises/articles/promises_04_futures.html)
#' package. (In short, the return value of `func` should be a
#' [`mirai`][mirai::mirai()], [`Future`][future::future()], `promise`,
#' or something else that [promises::as.promise()] understands.)
#'
#' It's also important that this logic does not read from any
#' reactive inputs/sources, as inputs may change after the function is
#' invoked; instead, if the function needs to access reactive inputs, it
#' should take parameters and the caller of the `invoke()` method should
#' read reactive inputs and pass them as arguments.
initialize = function(func) {
private$func <- func
private$rv_status <- reactiveVal("initial")
private$rv_value <- reactiveVal(NULL)
private$rv_error <- reactiveVal(NULL)
private$invocation_queue <- fastmap::fastqueue()
},
#' @description
#' Starts executing the long-running operation. If this `ExtendedTask` is
#' already running (meaning, a previous call to `invoke()` is not yet
#' complete) then enqueues this invocation until after the current
#' invocation, and any already-enqueued invocation, completes.
#'
#' @param ... Parameters to use for this invocation of the underlying
#' function. If reactive inputs are needed by the underlying function,
#' they should be read by the caller of `invoke` and passed in as
#' arguments.
invoke = function(...) {
args <- rlang::dots_list(..., .ignore_empty = "none")
call <- rlang::caller_call(n = 0)
if (
isolate(private$rv_status()) == "running" ||
private$invocation_queue$size() > 0
) {
private$invocation_queue$add(list(args = args, call = call))
} else {
private$do_invoke(args, call = call)
}
invisible(NULL)
},
#' @description
#' This is a reactive read that invalidates the caller when the task's
#' status changes.
#'
#' Returns one of the following values:
#'
#' * `"initial"`: This `ExtendedTask` has not yet been invoked
#' * `"running"`: An invocation is currently running
#' * `"success"`: An invocation completed successfully, and a value can be
#' retrieved via the `result()` method
#' * `"error"`: An invocation completed with an error, which will be
#' re-thrown if you call the `result()` method
status = function() {
private$rv_status()
},
#' @description
#' Attempts to read the results of the most recent invocation. This is a
#' reactive read that invalidates as the task's status changes.
#'
#' The actual behavior differs greatly depending on the current status of
#' the task:
#'
#' * `"initial"`: Throws a silent error (like [`req(FALSE)`][req()]). If
#' this happens during output rendering, the output will be blanked out.
#' * `"running"`: Throws a special silent error that, if it happens during
#' output rendering, makes the output appear "in progress" until further
#' notice.
#' * `"success"`: Returns the return value of the most recent invocation.
#' * `"error"`: Throws whatever error was thrown by the most recent
#' invocation.
#'
#' This method is intended to be called fairly naively by any output or
#' reactive expression that cares about the output--you just have to be
#' aware that if the result isn't ready for whatever reason, processing will
#' stop in much the same way as `req(FALSE)` does, but when the result is
#' ready you'll get invalidated, and when you run again the result should be
#' there.
#'
#' Note that the `result()` method is generally not meant to be used with
#' [observeEvent()], [eventReactive()], [bindEvent()], or [isolate()] as the
#' invalidation will be ignored.
result = function() {
switch (private$rv_status(),
running = req(FALSE, cancelOutput="progress"),
success = if (private$rv_value()$visible) {
private$rv_value()$value
} else {
invisible(private$rv_value()$value)
},
error = stop(private$rv_error()),
# default case (initial, cancelled)
req(FALSE)
)
}
),
private = list(
func = NULL,
# reactive value with "initial"|"running"|"success"|"error"
rv_status = NULL,
rv_value = NULL,
rv_error = NULL,
invocation_queue = NULL,
do_invoke = function(args, call = NULL) {
private$rv_status("running")
private$rv_value(NULL)
private$rv_error(NULL)
p <- promises::promise_resolve(
maskReactiveContext(do.call(private$func, args))
)
p <- promises::then(
p,
onFulfilled = function(value, .visible) {
private$on_success(list(value = value, visible = .visible))
},
onRejected = function(error) {
private$on_error(error, call = call)
}
)
promises::finally(p, onFinally = function() {
if (private$invocation_queue$size() > 0) {
next_call <- private$invocation_queue$remove()
private$do_invoke(next_call$args, next_call$call)
}
})
invisible(NULL)
},
on_error = function(err, call = NULL) {
cli::cli_warn(
"ERROR: An error occurred when invoking the ExtendedTask.",
parent = err,
call = call
)
private$rv_status("error")
private$rv_error(err)
},
on_success = function(value) {
private$rv_status("success")
private$rv_value(value)
}
)
)

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

@@ -0,0 +1,461 @@
# Generated by tools/updateFontAwesome.R: do not edit by hand
font_awesome_brands <- c(
"500px",
"accessible-icon",
"accusoft",
"acquisitions-incorporated",
"adn",
"adversal",
"affiliatetheme",
"airbnb",
"algolia",
"alipay",
"amazon",
"amazon-pay",
"amilia",
"android",
"angellist",
"angrycreative",
"angular",
"app-store",
"app-store-ios",
"apper",
"apple",
"apple-pay",
"artstation",
"asymmetrik",
"atlassian",
"audible",
"autoprefixer",
"avianex",
"aviato",
"aws",
"bandcamp",
"battle-net",
"behance",
"behance-square",
"bimobject",
"bitbucket",
"bitcoin",
"bity",
"black-tie",
"blackberry",
"blogger",
"blogger-b",
"bluetooth",
"bluetooth-b",
"bootstrap",
"btc",
"buffer",
"buromobelexperte",
"buy-n-large",
"buysellads",
"canadian-maple-leaf",
"cc-amazon-pay",
"cc-amex",
"cc-apple-pay",
"cc-diners-club",
"cc-discover",
"cc-jcb",
"cc-mastercard",
"cc-paypal",
"cc-stripe",
"cc-visa",
"centercode",
"centos",
"chrome",
"chromecast",
"cloudflare",
"cloudscale",
"cloudsmith",
"cloudversify",
"codepen",
"codiepie",
"confluence",
"connectdevelop",
"contao",
"cotton-bureau",
"cpanel",
"creative-commons",
"creative-commons-by",
"creative-commons-nc",
"creative-commons-nc-eu",
"creative-commons-nc-jp",
"creative-commons-nd",
"creative-commons-pd",
"creative-commons-pd-alt",
"creative-commons-remix",
"creative-commons-sa",
"creative-commons-sampling",
"creative-commons-sampling-plus",
"creative-commons-share",
"creative-commons-zero",
"critical-role",
"css3",
"css3-alt",
"cuttlefish",
"d-and-d",
"d-and-d-beyond",
"dailymotion",
"dashcube",
"deezer",
"delicious",
"deploydog",
"deskpro",
"dev",
"deviantart",
"dhl",
"diaspora",
"digg",
"digital-ocean",
"discord",
"discourse",
"dochub",
"docker",
"draft2digital",
"dribbble",
"dribbble-square",
"dropbox",
"drupal",
"dyalog",
"earlybirds",
"ebay",
"edge",
"edge-legacy",
"elementor",
"ello",
"ember",
"empire",
"envira",
"erlang",
"ethereum",
"etsy",
"evernote",
"expeditedssl",
"facebook",
"facebook-f",
"facebook-messenger",
"facebook-square",
"fantasy-flight-games",
"fedex",
"fedora",
"figma",
"firefox",
"firefox-browser",
"first-order",
"first-order-alt",
"firstdraft",
"flickr",
"flipboard",
"fly",
"font-awesome",
"font-awesome-alt",
"font-awesome-flag",
"font-awesome-logo-full",
"fonticons",
"fonticons-fi",
"fort-awesome",
"fort-awesome-alt",
"forumbee",
"foursquare",
"free-code-camp",
"freebsd",
"fulcrum",
"galactic-republic",
"galactic-senate",
"get-pocket",
"gg",
"gg-circle",
"git",
"git-alt",
"git-square",
"github",
"github-alt",
"github-square",
"gitkraken",
"gitlab",
"gitter",
"glide",
"glide-g",
"gofore",
"goodreads",
"goodreads-g",
"google",
"google-drive",
"google-pay",
"google-play",
"google-plus",
"google-plus-g",
"google-plus-square",
"google-wallet",
"gratipay",
"grav",
"gripfire",
"grunt",
"guilded",
"gulp",
"hacker-news",
"hacker-news-square",
"hackerrank",
"hips",
"hire-a-helper",
"hive",
"hooli",
"hornbill",
"hotjar",
"houzz",
"html5",
"hubspot",
"ideal",
"imdb",
"innosoft",
"instagram",
"instagram-square",
"instalod",
"intercom",
"internet-explorer",
"invision",
"ioxhost",
"itch-io",
"itunes",
"itunes-note",
"java",
"jedi-order",
"jenkins",
"jira",
"joget",
"joomla",
"js",
"js-square",
"jsfiddle",
"kaggle",
"keybase",
"keycdn",
"kickstarter",
"kickstarter-k",
"korvue",
"laravel",
"lastfm",
"lastfm-square",
"leanpub",
"less",
"line",
"linkedin",
"linkedin-in",
"linode",
"linux",
"lyft",
"magento",
"mailchimp",
"mandalorian",
"markdown",
"mastodon",
"maxcdn",
"mdb",
"medapps",
"medium",
"medium-m",
"medrt",
"meetup",
"megaport",
"mendeley",
"microblog",
"microsoft",
"mix",
"mixcloud",
"mixer",
"mizuni",
"modx",
"monero",
"napster",
"neos",
"nimblr",
"node",
"node-js",
"npm",
"ns8",
"nutritionix",
"octopus-deploy",
"odnoklassniki",
"odnoklassniki-square",
"old-republic",
"opencart",
"openid",
"opera",
"optin-monster",
"orcid",
"osi",
"page4",
"pagelines",
"palfed",
"patreon",
"paypal",
"penny-arcade",
"perbyte",
"periscope",
"phabricator",
"phoenix-framework",
"phoenix-squadron",
"php",
"pied-piper",
"pied-piper-alt",
"pied-piper-hat",
"pied-piper-pp",
"pied-piper-square",
"pinterest",
"pinterest-p",
"pinterest-square",
"playstation",
"product-hunt",
"pushed",
"python",
"qq",
"quinscape",
"quora",
"r-project",
"raspberry-pi",
"ravelry",
"react",
"reacteurope",
"readme",
"rebel",
"red-river",
"reddit",
"reddit-alien",
"reddit-square",
"redhat",
"renren",
"replyd",
"researchgate",
"resolving",
"rev",
"rocketchat",
"rockrms",
"rust",
"safari",
"salesforce",
"sass",
"schlix",
"scribd",
"searchengin",
"sellcast",
"sellsy",
"servicestack",
"shirtsinbulk",
"shopify",
"shopware",
"simplybuilt",
"sistrix",
"sith",
"sketch",
"skyatlas",
"skype",
"slack",
"slack-hash",
"slideshare",
"snapchat",
"snapchat-ghost",
"snapchat-square",
"soundcloud",
"sourcetree",
"speakap",
"speaker-deck",
"spotify",
"squarespace",
"stack-exchange",
"stack-overflow",
"stackpath",
"staylinked",
"steam",
"steam-square",
"steam-symbol",
"sticker-mule",
"strava",
"stripe",
"stripe-s",
"studiovinari",
"stumbleupon",
"stumbleupon-circle",
"superpowers",
"supple",
"suse",
"swift",
"symfony",
"teamspeak",
"telegram",
"telegram-plane",
"tencent-weibo",
"the-red-yeti",
"themeco",
"themeisle",
"think-peaks",
"tiktok",
"trade-federation",
"trello",
"tripadvisor",
"tumblr",
"tumblr-square",
"twitch",
"twitter",
"twitter-square",
"typo3",
"uber",
"ubuntu",
"uikit",
"umbraco",
"uncharted",
"uniregistry",
"unity",
"unsplash",
"untappd",
"ups",
"usb",
"usps",
"ussunnah",
"vaadin",
"viacoin",
"viadeo",
"viadeo-square",
"viber",
"vimeo",
"vimeo-square",
"vimeo-v",
"vine",
"vk",
"vnv",
"vuejs",
"watchman-monitoring",
"waze",
"weebly",
"weibo",
"weixin",
"whatsapp",
"whatsapp-square",
"whmcs",
"wikipedia-w",
"windows",
"wix",
"wizards-of-the-coast",
"wodu",
"wolf-pack-battalion",
"wordpress",
"wordpress-simple",
"wpbeginner",
"wpexplorer",
"wpforms",
"wpressr",
"xbox",
"xing",
"xing-square",
"y-combinator",
"yahoo",
"yammer",
"yandex",
"yandex-international",
"yarn",
"yelp",
"yoast",
"youtube",
"youtube-square",
"zhihu"
)

View File

@@ -1,27 +1,74 @@
# A scope where we can put mutable global state # A scope where we can put mutable global state
.globals <- new.env(parent = emptyenv()) .globals <- new.env(parent = emptyenv())
register_s3_method <- function(pkg, generic, class, fun = NULL) {
stopifnot(is.character(pkg), length(pkg) == 1)
stopifnot(is.character(generic), length(generic) == 1)
stopifnot(is.character(class), length(class) == 1)
if (is.null(fun)) {
fun <- get(paste0(generic, ".", class), envir = parent.frame())
} else {
stopifnot(is.function(fun))
}
if (pkg %in% loadedNamespaces()) {
registerS3method(generic, class, fun, envir = asNamespace(pkg))
}
# Always register hook in case pkg is loaded at some
# point the future (or, potentially, but less commonly,
# unloaded & reloaded)
setHook(
packageEvent(pkg, "onLoad"),
function(...) {
registerS3method(generic, class, fun, envir = asNamespace(pkg))
}
)
}
register_upgrade_message <- function(pkg, version) {
msg <- sprintf(
"This version of Shiny is designed to work with '%s' >= %s.
Please upgrade via install.packages('%s').",
pkg, version, pkg
)
if (pkg %in% loadedNamespaces() && !is_available(pkg, version)) {
packageStartupMessage(msg)
}
# Always register hook in case pkg is loaded at some
# point the future (or, potentially, but less commonly,
# unloaded & reloaded)
setHook(
packageEvent(pkg, "onLoad"),
function(...) {
if (!is_available(pkg, version)) packageStartupMessage(msg)
}
)
}
.onLoad <- function(libname, pkgname) { .onLoad <- function(libname, pkgname) {
# R's lazy-loading package scheme causes the private seed to be cached in the # R's lazy-loading package scheme causes the private seed to be cached in the
# package itself, making our PRNG completely deterministic. This line resets # package itself, making our PRNG completely deterministic. This line resets
# the private seed during load. # the private seed during load.
withPrivateSeed(set.seed(NULL)) withPrivateSeed(set.seed(NULL))
for (expr in on_load_exprs) { # Create this at the top level, but since the object is from a different
eval(expr, envir = environment(.onLoad)) # package, we don't want to bake it into the built binary package.
} restoreCtxStack <<- fastmap::faststack()
# Make sure these methods are available to knitr if shiny is loaded but not # Make sure these methods are available to knitr if shiny is loaded but not
# attached. # attached.
s3_register("knitr::knit_print", "reactive") register_s3_method("knitr", "knit_print", "reactive")
s3_register("knitr::knit_print", "shiny.appobj") register_s3_method("knitr", "knit_print", "shiny.appobj")
s3_register("knitr::knit_print", "shiny.render.function") register_s3_method("knitr", "knit_print", "shiny.render.function")
}
# Shiny 1.4.0 bumps jQuery 1.x to 3.x, which caused a problem
on_load_exprs <- list() # with static-rendering of htmlwidgets, and htmlwidgets 1.5
# Register an expression to be evaluated when the package is loaded (in the # includes a fix for this problem
# .onLoad function). # https://github.com/rstudio/shiny/issues/2630
on_load <- function(expr) { register_upgrade_message("htmlwidgets", 1.5)
on_load_exprs[[length(on_load_exprs) + 1]] <<- substitute(expr)
} }

186
R/graph.R
View File

@@ -1,3 +1,32 @@
# Check that the version of an suggested package satisfies the requirements
#
# @param package The name of the suggested package
# @param version The version of the package
check_suggested <- function(package, version = NULL) {
if (is_available(package, version)) {
return()
}
msg <- paste0(
sQuote(package),
if (is.na(version %||% NA)) "" else paste0("(>= ", version, ")"),
" must be installed for this functionality."
)
if (interactive()) {
message(msg, "\nWould you like to install it?")
if (utils::menu(c("Yes", "No")) == 1) {
return(utils::install.packages(package))
}
}
stop(msg, call. = FALSE)
}
# domain is like session # domain is like session
@@ -19,7 +48,7 @@ reactIdStr <- function(num) {
#' dependencies and execution in your application. #' dependencies and execution in your application.
#' #'
#' To use the reactive log visualizer, start with a fresh R session and #' To use the reactive log visualizer, start with a fresh R session and
#' run the command `reactlog::reactlog_enable()`; then launch your #' run the command `options(shiny.reactlog=TRUE)`; then launch your
#' application in the usual way (e.g. using [runApp()]). At #' application in the usual way (e.g. using [runApp()]). At
#' any time you can hit Ctrl+F3 (or for Mac users, Command+F3) in your #' any time you can hit Ctrl+F3 (or for Mac users, Command+F3) in your
#' web browser to launch the reactive log visualization. #' web browser to launch the reactive log visualization.
@@ -42,20 +71,16 @@ reactIdStr <- function(num) {
#' call `reactlogShow()` explicitly. #' call `reactlogShow()` explicitly.
#' #'
#' For security and performance reasons, do not enable #' For security and performance reasons, do not enable
#' `options(shiny.reactlog=TRUE)` (or `reactlog::reactlog_enable()`) in #' `shiny.reactlog` in production environments. When the option is
#' production environments. When the option is enabled, it's possible #' enabled, it's possible for any user of your app to see at least some
#' for any user of your app to see at least some of the source code of #' of the source code of your reactive expressions and observers.
#' your reactive expressions and observers. In addition, reactlog
#' should be considered a memory leak as it will constantly grow and
#' will never reset until the R session is restarted.
#' #'
#' @name reactlog #' @name reactlog
NULL NULL
#' @describeIn reactlog Return a list of reactive information. Can be used in #' @describeIn reactlog Return a list of reactive information. Can be used in conjunction with
#' conjunction with [reactlog::reactlog_show] to later display the reactlog #' [reactlog::reactlog_show] to later display the reactlog graph.
#' graph.
#' @export #' @export
reactlog <- function() { reactlog <- function() {
rLog$asList() rLog$asList()
@@ -69,35 +94,20 @@ reactlogShow <- function(time = TRUE) {
check_reactlog() check_reactlog()
reactlog::reactlog_show(reactlog(), time = time) reactlog::reactlog_show(reactlog(), time = time)
} }
#' @describeIn reactlog This function is deprecated. You should use [reactlogShow()]
#' @export
# legacy purposes
showReactLog <- function(time = TRUE) {
shinyDeprecated("1.2.0", "showReactLog()", "reactlogShow()")
#' @describeIn reactlog Resets the entire reactlog stack. Useful for debugging reactlogShow(time = time)
#' and removing all prior reactive history. }
#' @describeIn reactlog Resets the entire reactlog stack. Useful for debugging and removing all prior reactive history.
#' @export #' @export
reactlogReset <- function() { reactlogReset <- function() {
rLog$reset() rLog$reset()
} }
#' @describeIn reactlog Adds "mark" entry into the reactlog stack. This is
#' useful for programmatically adding a marked entry in the reactlog, rather
#' than using your keyboard's key combination.
#'
#' For example, we can _mark_ the reactlog at the beginning of an
#' `observeEvent`'s calculation:
#' ```r
#' observeEvent(input$my_event_trigger, {
#' # Add a mark in the reactlog
#' reactlogAddMark()
#' # Run your regular event reaction code here...
#' ....
#' })
#' ```
#' @param session The Shiny session to assign the mark to. Defaults to the
#' current session.
#' @export
reactlogAddMark <- function(session = getDefaultReactiveDomain()) {
rLog$userMark(session)
}
# called in "/reactlog" middleware # called in "/reactlog" middleware
renderReactlog <- function(sessionToken = NULL, time = TRUE) { renderReactlog <- function(sessionToken = NULL, time = TRUE) {
check_reactlog() check_reactlog()
@@ -107,15 +117,28 @@ renderReactlog <- function(sessionToken = NULL, time = TRUE) {
time = time time = time
) )
} }
check_reactlog <- function() { check_reactlog <- function() {
if (!is_installed("reactlog", reactlog_min_version)) { check_suggested("reactlog", reactlog_version())
rlang::check_installed("reactlog", reactlog_min_version) }
# read reactlog version from description file
# prevents version mismatch in code and description file
reactlog_version <- function() {
desc <- read.dcf(system.file("DESCRIPTION", package = "shiny", mustWork = TRUE))
suggests <- desc[1,"Suggests"][[1]]
suggests_pkgs <- strsplit(suggests, "\n")[[1]]
reactlog_info <- suggests_pkgs[grepl("reactlog", suggests_pkgs)]
if (length(reactlog_info) == 0) {
stop("reactlog can not be found in shiny DESCRIPTION file")
} }
reactlog_info <- sub("^[^\\(]*\\(", "", reactlog_info)
reactlog_info <- sub("\\)[^\\)]*$", "", reactlog_info)
reactlog_info <- sub("^[>= ]*", "", reactlog_info)
package_version(reactlog_info)
} }
# Should match the (suggested) version in DESCRIPTION file
reactlog_min_version <- "1.0.0"
RLog <- R6Class( RLog <- R6Class(
"RLog", "RLog",
@@ -123,6 +146,7 @@ RLog <- R6Class(
private = list( private = list(
option = "shiny.reactlog", option = "shiny.reactlog",
msgOption = "shiny.reactlog.console", msgOption = "shiny.reactlog.console",
appendEntry = function(domain, logEntry) { appendEntry = function(domain, logEntry) {
if (self$isLogging()) { if (self$isLogging()) {
sessionToken <- if (is.null(domain)) NULL else domain$token sessionToken <- if (is.null(domain)) NULL else domain$token
@@ -137,19 +161,20 @@ RLog <- R6Class(
public = list( public = list(
msg = "<MessageLogger>", msg = "<MessageLogger>",
logStack = "<Stack>", logStack = "<Stack>",
noReactIdLabel = "NoCtxReactId", noReactIdLabel = "NoCtxReactId",
noReactId = reactIdStr("NoCtxReactId"), noReactId = reactIdStr("NoCtxReactId"),
dummyReactIdLabel = "DummyReactId", dummyReactIdLabel = "DummyReactId",
dummyReactId = reactIdStr("DummyReactId"), dummyReactId = reactIdStr("DummyReactId"),
asList = function() { asList = function() {
ret <- self$logStack$as_list() ret <- self$logStack$as_list()
attr(ret, "version") <- "1" attr(ret, "version") <- "1"
ret ret
}, },
ctxIdStr = function(ctxId) { ctxIdStr = function(ctxId) {
if (is.null(ctxId) || identical(ctxId, "")) { if (is.null(ctxId) || identical(ctxId, "")) return(NULL)
return(NULL)
}
paste0("ctx", ctxId) paste0("ctx", ctxId)
}, },
namesIdStr = function(reactId) { namesIdStr = function(reactId) {
@@ -164,6 +189,7 @@ RLog <- R6Class(
keyIdStr = function(reactId, key) { keyIdStr = function(reactId, key) {
paste0(reactId, "$", key) paste0(reactId, "$", key)
}, },
valueStr = function(value, n = 200) { valueStr = function(value, n = 200) {
if (!self$isLogging()) { if (!self$isLogging()) {
# return a placeholder string to avoid calling str # return a placeholder string to avoid calling str
@@ -173,9 +199,10 @@ RLog <- R6Class(
# only capture the first level of the object # only capture the first level of the object
utils::capture.output(utils::str(value, max.level = 1)) utils::capture.output(utils::str(value, max.level = 1))
}) })
outputTxt <- paste0(output, collapse = "\n") outputTxt <- paste0(output, collapse="\n")
msg$shortenString(outputTxt, n = n) msg$shortenString(outputTxt, n = n)
}, },
initialize = function(rlogOption = "shiny.reactlog", msgOption = "shiny.reactlog.console") { initialize = function(rlogOption = "shiny.reactlog", msgOption = "shiny.reactlog.console") {
private$option <- rlogOption private$option <- rlogOption
private$msgOption <- msgOption private$msgOption <- msgOption
@@ -195,6 +222,7 @@ RLog <- R6Class(
isLogging = function() { isLogging = function() {
isTRUE(getOption(private$option, FALSE)) isTRUE(getOption(private$option, FALSE))
}, },
define = function(reactId, value, label, type, domain) { define = function(reactId, value, label, type, domain) {
valueStr <- self$valueStr(value) valueStr <- self$valueStr(value)
if (msg$hasReact(reactId)) { if (msg$hasReact(reactId)) {
@@ -225,10 +253,9 @@ RLog <- R6Class(
defineObserver = function(reactId, label, domain) { defineObserver = function(reactId, label, domain) {
self$define(reactId, value = NULL, label, "observer", domain) self$define(reactId, value = NULL, label, "observer", domain)
}, },
dependsOn = function(reactId, depOnReactId, ctxId, domain) { dependsOn = function(reactId, depOnReactId, ctxId, domain) {
if (is.null(reactId)) { if (is.null(reactId)) return()
return()
}
ctxId <- ctxIdStr(ctxId) ctxId <- ctxIdStr(ctxId)
msg$log("dependsOn:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId)) msg$log("dependsOn:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId))
private$appendEntry(domain, list( private$appendEntry(domain, list(
@@ -241,6 +268,7 @@ RLog <- R6Class(
dependsOnKey = function(reactId, depOnReactId, key, ctxId, domain) { dependsOnKey = function(reactId, depOnReactId, key, ctxId, domain) {
self$dependsOn(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain) self$dependsOn(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain)
}, },
dependsOnRemove = function(reactId, depOnReactId, ctxId, domain) { dependsOnRemove = function(reactId, depOnReactId, ctxId, domain) {
ctxId <- self$ctxIdStr(ctxId) ctxId <- self$ctxIdStr(ctxId)
msg$log("dependsOnRemove:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId)) msg$log("dependsOnRemove:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId))
@@ -254,6 +282,7 @@ RLog <- R6Class(
dependsOnKeyRemove = function(reactId, depOnReactId, key, ctxId, domain) { dependsOnKeyRemove = function(reactId, depOnReactId, key, ctxId, domain) {
self$dependsOnRemove(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain) self$dependsOnRemove(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain)
}, },
createContext = function(ctxId, label, type, prevCtxId, domain) { createContext = function(ctxId, label, type, prevCtxId, domain) {
ctxId <- self$ctxIdStr(ctxId) ctxId <- self$ctxIdStr(ctxId)
prevCtxId <- self$ctxIdStr(prevCtxId) prevCtxId <- self$ctxIdStr(prevCtxId)
@@ -264,9 +293,10 @@ RLog <- R6Class(
label = msg$shortenString(label), label = msg$shortenString(label),
type = type, type = type,
prevCtxId = prevCtxId, prevCtxId = prevCtxId,
srcref = as.vector(attr(label, "srcref")), srcfile = attr(label, "srcfile") srcref = as.vector(attr(label, "srcref")), srcfile=attr(label, "srcfile")
)) ))
}, },
enter = function(reactId, ctxId, type, domain) { enter = function(reactId, ctxId, type, domain) {
ctxId <- self$ctxIdStr(ctxId) ctxId <- self$ctxIdStr(ctxId)
if (identical(type, "isolate")) { if (identical(type, "isolate")) {
@@ -309,6 +339,7 @@ RLog <- R6Class(
)) ))
} }
}, },
valueChange = function(reactId, value, domain) { valueChange = function(reactId, value, domain) {
valueStr <- self$valueStr(value) valueStr <- self$valueStr(value)
msg$log("valueChange:", msg$reactStr(reactId), msg$valueStr(valueStr)) msg$log("valueChange:", msg$reactStr(reactId), msg$valueStr(valueStr))
@@ -330,6 +361,8 @@ RLog <- R6Class(
valueChangeKey = function(reactId, key, value, domain) { valueChangeKey = function(reactId, key, value, domain) {
self$valueChange(self$keyIdStr(reactId, key), value, domain) self$valueChange(self$keyIdStr(reactId, key), value, domain)
}, },
invalidateStart = function(reactId, ctxId, type, domain) { invalidateStart = function(reactId, ctxId, type, domain) {
ctxId <- self$ctxIdStr(ctxId) ctxId <- self$ctxIdStr(ctxId)
if (identical(type, "isolate")) { if (identical(type, "isolate")) {
@@ -372,6 +405,7 @@ RLog <- R6Class(
)) ))
} }
}, },
invalidateLater = function(reactId, runningCtx, millis, domain) { invalidateLater = function(reactId, runningCtx, millis, domain) {
msg$log("invalidateLater: ", millis, "ms", msg$reactStr(reactId), msg$ctxStr(runningCtx)) msg$log("invalidateLater: ", millis, "ms", msg$reactStr(reactId), msg$ctxStr(runningCtx))
private$appendEntry(domain, list( private$appendEntry(domain, list(
@@ -381,12 +415,14 @@ RLog <- R6Class(
millis = millis millis = millis
)) ))
}, },
idle = function(domain = NULL) { idle = function(domain = NULL) {
msg$log("idle") msg$log("idle")
private$appendEntry(domain, list( private$appendEntry(domain, list(
action = "idle" action = "idle"
)) ))
}, },
asyncStart = function(domain = NULL) { asyncStart = function(domain = NULL) {
msg$log("asyncStart") msg$log("asyncStart")
private$appendEntry(domain, list( private$appendEntry(domain, list(
@@ -399,6 +435,7 @@ RLog <- R6Class(
action = "asyncStop" action = "asyncStop"
)) ))
}, },
freezeReactiveVal = function(reactId, domain) { freezeReactiveVal = function(reactId, domain) {
msg$log("freeze:", msg$reactStr(reactId)) msg$log("freeze:", msg$reactStr(reactId))
private$appendEntry(domain, list( private$appendEntry(domain, list(
@@ -409,6 +446,7 @@ RLog <- R6Class(
freezeReactiveKey = function(reactId, key, domain) { freezeReactiveKey = function(reactId, key, domain) {
self$freezeReactiveVal(self$keyIdStr(reactId, key), domain) self$freezeReactiveVal(self$keyIdStr(reactId, key), domain)
}, },
thawReactiveVal = function(reactId, domain) { thawReactiveVal = function(reactId, domain) {
msg$log("thaw:", msg$reactStr(reactId)) msg$log("thaw:", msg$reactStr(reactId))
private$appendEntry(domain, list( private$appendEntry(domain, list(
@@ -419,60 +457,54 @@ RLog <- R6Class(
thawReactiveKey = function(reactId, key, domain) { thawReactiveKey = function(reactId, key, domain) {
self$thawReactiveVal(self$keyIdStr(reactId, key), domain) self$thawReactiveVal(self$keyIdStr(reactId, key), domain)
}, },
userMark = function(domain = NULL) { userMark = function(domain = NULL) {
msg$log("userMark") msg$log("userMark")
private$appendEntry(domain, list( private$appendEntry(domain, list(
action = "userMark" action = "userMark"
)) ))
} }
) )
) )
MessageLogger <- R6Class( MessageLogger = R6Class(
"MessageLogger", "MessageLogger",
portable = FALSE, portable = FALSE,
public = list( public = list(
depth = 0L, depth = 0L,
reactCache = list(), reactCache = list(),
option = "shiny.reactlog.console", option = "shiny.reactlog.console",
initialize = function(option = "shiny.reactlog.console", depth = 0L) { initialize = function(option = "shiny.reactlog.console", depth = 0L) {
if (!missing(depth)) self$depth <- depth if (!missing(depth)) self$depth <- depth
if (!missing(option)) self$option <- option if (!missing(option)) self$option <- option
}, },
isLogging = function() { isLogging = function() {
isTRUE(getOption(self$option)) isTRUE(getOption(self$option))
}, },
isNotLogging = function() { isNotLogging = function() {
!isTRUE(getOption(self$option)) ! isTRUE(getOption(self$option))
}, },
depthIncrement = function() { depthIncrement = function() {
if (self$isNotLogging()) { if (self$isNotLogging()) return(NULL)
return(NULL)
}
self$depth <- self$depth + 1L self$depth <- self$depth + 1L
}, },
depthDecrement = function() { depthDecrement = function() {
if (self$isNotLogging()) { if (self$isNotLogging()) return(NULL)
return(NULL)
}
self$depth <- self$depth - 1L self$depth <- self$depth - 1L
}, },
hasReact = function(reactId) { hasReact = function(reactId) {
if (self$isNotLogging()) { if (self$isNotLogging()) return(FALSE)
return(FALSE)
}
!is.null(self$getReact(reactId)) !is.null(self$getReact(reactId))
}, },
getReact = function(reactId, force = FALSE) { getReact = function(reactId, force = FALSE) {
if (identical(force, FALSE) && self$isNotLogging()) { if (identical(force, FALSE) && self$isNotLogging()) return(NULL)
return(NULL)
}
self$reactCache[[reactId]] self$reactCache[[reactId]]
}, },
setReact = function(reactObj, force = FALSE) { setReact = function(reactObj, force = FALSE) {
if (identical(force, FALSE) && self$isNotLogging()) { if (identical(force, FALSE) && self$isNotLogging()) return(NULL)
return(NULL)
}
self$reactCache[[reactObj$reactId]] <- reactObj self$reactCache[[reactObj$reactId]] <- reactObj
}, },
shortenString = function(txt, n = 250) { shortenString = function(txt, n = 250) {
@@ -487,21 +519,17 @@ MessageLogger <- R6Class(
return(txt) return(txt)
}, },
singleLine = function(txt) { singleLine = function(txt) {
gsub("([^\\])\\n", "\\1\\\\n", txt) gsub("[^\\]\\n", "\\\\n", txt)
}, },
valueStr = function(valueStr) { valueStr = function(valueStr) {
paste0( paste0(
" '", self$shortenString(self$singleLine(valueStr)), "'" " '", self$shortenString(self$singleLine(valueStr)), "'"
) )
}, },
reactStr = function(reactId) { reactStr = function(reactId) {
if (self$isNotLogging()) { if (self$isNotLogging()) return(NULL)
return(NULL)
}
reactInfo <- self$getReact(reactId) reactInfo <- self$getReact(reactId)
if (is.null(reactInfo)) { if (is.null(reactInfo)) return(" <UNKNOWN_REACTID>")
return(" <UNKNOWN_REACTID>")
}
paste0( paste0(
" ", reactInfo$reactId, ":'", self$shortenString(self$singleLine(reactInfo$label)), "'" " ", reactInfo$reactId, ":'", self$shortenString(self$singleLine(reactInfo$label)), "'"
) )
@@ -510,15 +538,11 @@ MessageLogger <- R6Class(
self$ctxStr(ctxId = NULL, type = type) self$ctxStr(ctxId = NULL, type = type)
}, },
ctxStr = function(ctxId = NULL, type = NULL) { ctxStr = function(ctxId = NULL, type = NULL) {
if (self$isNotLogging()) { if (self$isNotLogging()) return(NULL)
return(NULL)
}
self$ctxPrevCtxStr(ctxId = ctxId, prevCtxId = NULL, type = type) self$ctxPrevCtxStr(ctxId = ctxId, prevCtxId = NULL, type = type)
}, },
ctxPrevCtxStr = function(ctxId = NULL, prevCtxId = NULL, type = NULL, preCtxIdTxt = " in ") { ctxPrevCtxStr = function(ctxId = NULL, prevCtxId = NULL, type = NULL, preCtxIdTxt = " in ") {
if (self$isNotLogging()) { if (self$isNotLogging()) return(NULL)
return(NULL)
}
paste0( paste0(
if (!is.null(ctxId)) paste0(preCtxIdTxt, ctxId), if (!is.null(ctxId)) paste0(preCtxIdTxt, ctxId),
if (!is.null(prevCtxId)) paste0(" from ", prevCtxId), if (!is.null(prevCtxId)) paste0(" from ", prevCtxId),
@@ -526,9 +550,7 @@ MessageLogger <- R6Class(
) )
}, },
log = function(...) { log = function(...) {
if (self$isNotLogging()) { if (self$isNotLogging()) return(NULL)
return(NULL)
}
msg <- paste0( msg <- paste0(
paste0(rep("= ", depth), collapse = ""), "- ", paste0(..., collapse = ""), paste0(rep("= ", depth), collapse = ""), "- ", paste0(..., collapse = ""),
collapse = "" collapse = ""
@@ -538,6 +560,4 @@ MessageLogger <- R6Class(
) )
) )
on_load({ rLog <- RLog$new("shiny.reactlog", "shiny.reactlog.console")
rLog <- RLog$new("shiny.reactlog", "shiny.reactlog.console")
})

View File

@@ -14,7 +14,7 @@ NULL
#' depending on the values in the query string / hash (e.g. instead of basing #' depending on the values in the query string / hash (e.g. instead of basing
#' the conditional on an input or a calculated reactive, you can base it on the #' the conditional on an input or a calculated reactive, you can base it on the
#' query string). However, note that, if you're changing the query string / hash #' query string). However, note that, if you're changing the query string / hash
#' programmatically from within the server code, you must use #' programatically from within the server code, you must use
#' `updateQueryString(_yourNewQueryString_, mode = "push")`. The default #' `updateQueryString(_yourNewQueryString_, mode = "push")`. The default
#' `mode` for `updateQueryString` is `"replace"`, which doesn't #' `mode` for `updateQueryString` is `"replace"`, which doesn't
#' raise any events, so any observers or reactives that depend on it will #' raise any events, so any observers or reactives that depend on it will

View File

@@ -40,14 +40,11 @@ createWebDependency <- function(dependency, scrubFile = TRUE) {
# Given a Shiny tag object, process singletons and dependencies. Returns a list # Given a Shiny tag object, process singletons and dependencies. Returns a list
# with rendered HTML and dependency objects. # with rendered HTML and dependency objects.
# This implementation is very similar to renderTags(), but ignores
# <head> handling (it should only be used after the user session has started)
processDeps <- function(tags, session) { processDeps <- function(tags, session) {
tags <- utils::getFromNamespace("tagify", "htmltools")(tags) ui <- takeSingletons(tags, session$singletons, desingleton=FALSE)$ui
ui <- takeSingletons(tags, session$singletons, desingleton = FALSE)$ui
ui <- surroundSingletons(ui) ui <- surroundSingletons(ui)
dependencies <- lapply( dependencies <- lapply(
resolveDependencies(findDependencies(ui, tagify = FALSE)), resolveDependencies(findDependencies(ui)),
createWebDependency createWebDependency
) )
names(dependencies) <- NULL names(dependencies) <- NULL

View File

@@ -20,6 +20,7 @@
#' `delay` milliseconds before sending an event. #' `delay` milliseconds before sending an event.
#' @seealso [brushOpts()] for brushing events. #' @seealso [brushOpts()] for brushing events.
#' @export #' @export
#' @keywords internal
clickOpts <- function(id, clip = TRUE) { clickOpts <- function(id, clip = TRUE) {
if (is.null(id)) if (is.null(id))
stop("id must not be NULL") stop("id must not be NULL")

View File

@@ -182,8 +182,8 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
# $ xmax : num 3.78 # $ xmax : num 3.78
# $ ymin : num 17.1 # $ ymin : num 17.1
# $ ymax : num 20.4 # $ ymax : num 20.4
# $ panelvar1: chr "6" # $ panelvar1: int 6
# $ panelvar2: chr "0 # $ panelvar2: int 0
# $ coords_css:List of 4 # $ coords_css:List of 4
# ..$ xmin: int 260 # ..$ xmin: int 260
# ..$ xmax: int 298 # ..$ xmax: int 298
@@ -267,7 +267,6 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
stop("nearPoints: `yvar` ('", yvar ,"') not in names of input") stop("nearPoints: `yvar` ('", yvar ,"') not in names of input")
# Extract data values from the data frame # Extract data values from the data frame
coordinfo <- fortifyDiscreteLimits(coordinfo)
x <- asNumber(df[[xvar]], coordinfo$domain$discrete_limits$x) x <- asNumber(df[[xvar]], coordinfo$domain$discrete_limits$x)
y <- asNumber(df[[yvar]], coordinfo$domain$discrete_limits$y) y <- asNumber(df[[yvar]], coordinfo$domain$discrete_limits$y)
@@ -367,8 +366,8 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
# $ img_css_ratio:List of 2 # $ img_css_ratio:List of 2
# ..$ x: num 1.25 # ..$ x: num 1.25
# ..$ y: num 1.25 # ..$ y: num 1.25
# $ panelvar1 : chr "6" # $ panelvar1 : int 6
# $ panelvar2 : chr "0" # $ panelvar2 : int 0
# $ mapping :List of 4 # $ mapping :List of 4
# ..$ x : chr "wt" # ..$ x : chr "wt"
# ..$ y : chr "mpg" # ..$ y : chr "mpg"
@@ -393,7 +392,6 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
# an input brush # an input brush
within_brush <- function(vals, brush, var = "x") { within_brush <- function(vals, brush, var = "x") {
var <- match.arg(var, c("x", "y")) var <- match.arg(var, c("x", "y"))
brush <- fortifyDiscreteLimits(brush)
vals <- asNumber(vals, brush$domain$discrete_limits[[var]]) vals <- asNumber(vals, brush$domain$discrete_limits[[var]])
# It's possible for a non-missing data values to not # It's possible for a non-missing data values to not
# map to the axis limits, for example: # map to the axis limits, for example:
@@ -416,43 +414,11 @@ asNumber <- function(x, levels = NULL) {
as.numeric(x) as.numeric(x)
} }
# Ensure the discrete limits/levels of a coordmap received
# from the client matches the data structure sent the client.
#
# When we construct the coordmap (in getGgplotCoordmap()),
# we save a character vector which may contain missing values
# (e.g., c("a", "b", NA)). When that same character is received
# from the client, it runs through decodeMessage() which sets
# simplifyVector=FALSE, which means NA are replaced by NULL
# (because jsonlite::fromJSON('["a", "b", null]') -> list("a", "b", NULL))
#
# Thankfully, it doesn't seem like it's meaningful for limits to
# contains a NULL in the 1st place, so we simply treat NULL like NA.
# For more context, https://github.com/rstudio/shiny/issues/2666
fortifyDiscreteLimits <- function(coord) {
# Note that discrete_limits$x/y are populated iff
# x/y are discrete mappings
coord$domain$discrete_limits <- lapply(
coord$domain$discrete_limits,
function(var) {
# if there is an 'explicit' NULL, then the limits are NA
if (is.null(var)) return(NA)
vapply(var, function(x) {
if (is.null(x) || isTRUE(is.na(x))) NA_character_ else x
}, character(1))
}
)
coord
}
# Given a panelvar value and a vector x, return logical vector indicating which # Given a panelvar value and a vector x, return logical vector indicating which
# items match the panelvar value. Because the panelvar value is always a # items match the panelvar value. Because the panelvar value is always a
# string but the vector could be numeric, it might be necessary to coerce the # string but the vector could be numeric, it might be necessary to coerce the
# panelvar to a number before comparing to the vector. # panelvar to a number before comparing to the vector.
panelMatch <- function(search_value, x) { panelMatch <- function(search_value, x) {
if (is.null(search_value)) return(is.na(x))
if (is.numeric(x)) search_value <- as.numeric(search_value) if (is.numeric(x)) search_value <- as.numeric(search_value)
x == search_value x == search_value
} }

View File

@@ -1,23 +1,22 @@
startPNG <- function(filename, width, height, res, ...) { startPNG <- function(filename, width, height, res, ...) {
pngfun <- if ((getOption('shiny.useragg') %||% TRUE) && is_installed("ragg")) { # shiny.useragg is an experimental option that isn't officially supported or
ragg::agg_png # documented. It's here in the off chance that someone really wants
# to use ragg (say, instead of showtext, for custom font rendering).
# In the next shiny release, this option will likely be superseded in
# favor of a fully customizable graphics device option
if ((getOption('shiny.useragg') %||% FALSE) && is_available("ragg")) {
pngfun <- ragg::agg_png
} else if (capabilities("aqua")) { } else if (capabilities("aqua")) {
# i.e., png(type = 'quartz') # i.e., png(type = 'quartz')
grDevices::png pngfun <- grDevices::png
} else if ((getOption('shiny.usecairo') %||% TRUE) && is_installed("Cairo")) { } else if ((getOption('shiny.usecairo') %||% TRUE) && is_available("Cairo")) {
Cairo::CairoPNG pngfun <- Cairo::CairoPNG
} else { } else {
# i.e., png(type = 'cairo') # i.e., png(type = 'cairo')
grDevices::png pngfun <- grDevices::png
} }
args <- list2(filename = filename, width = width, height = height, res = res, ...) args <- rlang::list2(filename=filename, width=width, height=height, res=res, ...)
# It's possible for width/height to be NULL/numeric(0) (e.g., when using
# suspendWhenHidden=F w/ tabsetPanel(), see rstudio/shiny#1409), so when
# this happens let the device determine what the default size should be.
if (length(args$width) == 0) args$width <- NULL
if (length(args$height) == 0) args$height <- NULL
# Set a smarter default for the device's bg argument (based on thematic's global state). # Set a smarter default for the device's bg argument (based on thematic's global state).
# Note that, technically, this is really only needed for CairoPNG, since the other # Note that, technically, this is really only needed for CairoPNG, since the other
@@ -58,35 +57,33 @@ startPNG <- function(filename, width, height, res, ...) {
grDevices::dev.cur() grDevices::dev.cur()
} }
#' Capture a plot as a PNG file. #' Run a plotting function and save the output as a PNG
#' #'
#' The PNG graphics device used is determined in the following order: #' This function returns the name of the PNG file that it generates. In
#' * If the ragg package is installed (and the `shiny.useragg` is not #' essence, it calls `png()`, then `func()`, then `dev.off()`.
#' set to `FALSE`), then use [ragg::agg_png()]. #' So `func` must be a function that will generate a plot when used this
#' * If a quartz device is available (i.e., `capabilities("aqua")` is #' way.
#' `TRUE`), then use `png(type = "quartz")`.
#' * If the Cairo package is installed (and the `shiny.usecairo` option
#' is not set to `FALSE`), then use [Cairo::CairoPNG()].
#' * Otherwise, use [grDevices::png()]. In this case, Linux and Windows
#' may not antialias some point shapes, resulting in poor quality output.
#' #'
#' @details #' For output, it will try to use the following devices, in this order:
#' A `NULL` value provided to `width` or `height` is ignored (i.e., the #' quartz (via [grDevices::png()]), then [Cairo::CairoPNG()],
#' default `width` or `height` of the graphics device is used). #' and finally [grDevices::png()]. This is in order of quality of
#' output. Notably, plain `png` output on Linux and Windows may not
#' antialias some point shapes, resulting in poor quality output.
#'
#' In some cases, `Cairo()` provides output that looks worse than
#' `png()`. To disable Cairo output for an app, use
#' `options(shiny.usecairo=FALSE)`.
#' #'
#' @param func A function that generates a plot. #' @param func A function that generates a plot.
#' @param filename The name of the output file. Defaults to a temp file with #' @param filename The name of the output file. Defaults to a temp file with
#' extension `.png`. #' extension `.png`.
#' @param width Width in pixels. #' @param width Width in pixels.
#' @param height Height in pixels. #' @param height Height in pixels.
#' @param res Resolution in pixels per inch. This value is passed to the #' @param res Resolution in pixels per inch. This value is passed to
#' graphics device. Note that this affects the resolution of PNG rendering in #' [grDevices::png()]. Note that this affects the resolution of PNG rendering in
#' R; it won't change the actual ppi of the browser. #' R; it won't change the actual ppi of the browser.
#' @param ... Arguments to be passed through to the graphics device. These can #' @param ... Arguments to be passed through to [grDevices::png()].
#' be used to set the width, height, background color, etc. #' These can be used to set the width, height, background color, etc.
#'
#' @return A path to the newly generated PNG file.
#'
#' @export #' @export
plotPNG <- function(func, filename=tempfile(fileext='.png'), plotPNG <- function(func, filename=tempfile(fileext='.png'),
width=400, height=400, res=72, ...) { width=400, height=400, res=72, ...) {

View File

@@ -7,8 +7,6 @@
#' @param label The contents of the button or link--usually a text label, but #' @param label The contents of the button or link--usually a text label, but
#' you could also use any other HTML, like an image. #' you could also use any other HTML, like an image.
#' @param icon An optional [icon()] to appear on the button. #' @param icon An optional [icon()] to appear on the button.
#' @param disabled If `TRUE`, the button will not be clickable. Use
#' [updateActionButton()] to dynamically enable/disable the button.
#' @param ... Named attributes to be applied to the button or link. #' @param ... Named attributes to be applied to the button or link.
#' #'
#' @family input elements #' @family input elements
@@ -51,29 +49,16 @@
#' * Event handlers (e.g., [observeEvent()], [eventReactive()]) won't execute on initial load. #' * Event handlers (e.g., [observeEvent()], [eventReactive()]) won't execute on initial load.
#' * Input validation (e.g., [req()], [need()]) will fail on initial load. #' * Input validation (e.g., [req()], [need()]) will fail on initial load.
#' @export #' @export
actionButton <- function(inputId, label, icon = NULL, width = NULL, actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
disabled = FALSE, ...) {
value <- restoreInput(id = inputId, default = NULL) value <- restoreInput(id = inputId, default = NULL)
icon <- validateIcon(icon) tags$button(id=inputId,
if (!is.null(icon)) {
icon <- span(icon, class = "action-icon")
}
if (!is.null(label)) {
label <- span(label, class = "action-label")
}
tags$button(
id = inputId,
style = css(width = validateCssUnit(width)), style = css(width = validateCssUnit(width)),
type = "button", type="button",
class = "btn btn-default action-button", class="btn btn-default action-button",
`data-val` = value, `data-val` = value,
disabled = if (isTRUE(disabled)) NA else NULL, list(validateIcon(icon), label),
icon, label,
... ...
) )
} }
@@ -83,40 +68,30 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL,
actionLink <- function(inputId, label, icon = NULL, ...) { actionLink <- function(inputId, label, icon = NULL, ...) {
value <- restoreInput(id = inputId, default = NULL) value <- restoreInput(id = inputId, default = NULL)
icon <- validateIcon(icon) tags$a(id=inputId,
href="#",
if (!is.null(icon)) { class="action-button",
icon <- span(icon, class = "action-icon")
}
if (!is.null(label)) {
label <- span(label, class = "action-label")
}
tags$a(
id = inputId,
href = "#",
class = "action-button action-link",
`data-val` = value, `data-val` = value,
icon, label, list(validateIcon(icon), label),
... ...
) )
} }
# Throw an informative warning if icon isn't html-ish # 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) { validateIcon <- function(icon) {
if (length(icon) == 0) { if (is.null(icon) || identical(icon, character(0))) {
return(icon) 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")
} }
if (!isTagLike(icon)) {
rlang::warn(
c(
"It appears that a non-HTML value was provided to `icon`.",
i = "Try using a `shiny::icon()` (or an equivalent) to get an icon."
),
class = "shiny-validate-icon"
)
}
icon
} }

View File

@@ -31,7 +31,7 @@ checkboxInput <- function(inputId, label, value = FALSE, width = NULL) {
value <- restoreInput(id = inputId, default = value) value <- restoreInput(id = inputId, default = value)
inputTag <- tags$input(id = inputId, type="checkbox", class = "shiny-input-checkbox") inputTag <- tags$input(id = inputId, type="checkbox")
if (!is.null(value) && value) if (!is.null(value) && value)
inputTag$attribs$checked <- "checked" inputTag$attribs$checked <- "checked"

View File

@@ -133,13 +133,14 @@ dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
} }
datePickerVersion <- "1.9.0"
datePickerDependency <- function(theme) { datePickerDependency <- function(theme) {
list( list(
htmlDependency( htmlDependency(
name = "bootstrap-datepicker-js", name = "bootstrap-datepicker-js",
version = version_bs_date_picker, version = datePickerVersion,
src = "www/shared/datepicker", src = c(href = "shared/datepicker"),
package = "shiny",
script = if (getOption("shiny.minified", TRUE)) "js/bootstrap-datepicker.min.js" script = if (getOption("shiny.minified", TRUE)) "js/bootstrap-datepicker.min.js"
else "js/bootstrap-datepicker.js", else "js/bootstrap-datepicker.js",
# Need to enable noConflict mode. See #1346. # Need to enable noConflict mode. See #1346.
@@ -153,28 +154,23 @@ datePickerDependency <- function(theme) {
) )
} }
datePickerSass <- function() {
sass::sass_file(
system_file(package = "shiny", "www/shared/datepicker/scss/build3.scss")
)
}
datePickerCSS <- function(theme) { datePickerCSS <- function(theme) {
if (!is_bs_theme(theme)) { if (!is_bs_theme(theme)) {
return(htmlDependency( return(htmlDependency(
name = "bootstrap-datepicker-css", name = "bootstrap-datepicker-css",
version = version_bs_date_picker, version = datePickerVersion,
src = "www/shared/datepicker", src = c(href = "shared/datepicker"),
package = "shiny",
stylesheet = "css/bootstrap-datepicker3.min.css" stylesheet = "css/bootstrap-datepicker3.min.css"
)) ))
} }
scss_file <- system.file(package = "shiny", "www/shared/datepicker/scss/build3.scss")
bslib::bs_dependency( bslib::bs_dependency(
input = datePickerSass(), input = sass::sass_file(scss_file),
theme = theme, theme = theme,
name = "bootstrap-datepicker", name = "bootstrap-datepicker",
version = version_bs_date_picker, version = datePickerVersion,
cache_key_extra = get_package_version("shiny") cache_key_extra = shinyPackageVersion()
) )
} }

View File

@@ -2,13 +2,8 @@
#' #'
#' Create a file upload control that can be used to upload one or more files. #' Create a file upload control that can be used to upload one or more files.
#' #'
#' Whenever a file upload completes, the corresponding input variable is set to #' Whenever a file upload completes, the corresponding input variable is set
#' a dataframe. See the `Server value` section. #' to a dataframe. See the `Server value` section.
#'
#' Each time files are uploaded, they are written to a new random subdirectory
#' inside of R's process-level temporary directory. The Shiny user session keeps
#' track of all uploads in the session, and when the session ends, Shiny deletes
#' all of the subdirectories where files where uploaded to.
#' #'
#' @family input elements #' @family input elements
#' #'
@@ -16,30 +11,19 @@
#' @param multiple Whether the user should be allowed to select and upload #' @param multiple Whether the user should be allowed to select and upload
#' multiple files at once. **Does not work on older browsers, including #' multiple files at once. **Does not work on older browsers, including
#' Internet Explorer 9 and earlier.** #' Internet Explorer 9 and earlier.**
#' @param accept A character vector of "unique file type specifiers" which gives #' @param accept A character vector of "unique file type specifiers" which
#' the browser a hint as to the type of file the server expects. Many browsers #' gives the browser a hint as to the type of file the server expects.
#' use this prevent the user from selecting an invalid file. #' Many browsers use this prevent the user from selecting an invalid file.
#' #'
#' A unique file type specifier can be: #' A unique file type specifier can be:
#' * A case insensitive extension like `.csv` or `.rds`. #' * A case insensitive extension like `.csv` or `.rds`.
#' * A valid MIME type, like `text/plain` or `application/pdf` #' * A valid MIME type, like `text/plain` or `application/pdf`
#' * One of `audio/*`, `video/*`, or `image/*` meaning any audio, video, #' * One of `audio/*`, `video/*`, or `image/*` meaning any audio, video,
#' or image type, respectively. #' or image type, respectively.
#' @param buttonLabel The label used on the button. Can be text or an HTML tag #' @param buttonLabel The label used on the button. Can be text or an HTML tag
#' object. #' object.
#' @param placeholder The text to show before a file has been uploaded. #' @param placeholder The text to show before a file has been uploaded.
#' @param capture What source to use for capturing image, audio or video data.
#' This attribute facilitates user access to a device's media capture
#' mechanism, such as a camera, or microphone, from within a file upload
#' control.
#' #'
#' A value of `user` indicates that the user-facing camera and/or microphone
#' should be used. A value of `environment` specifies that the outward-facing
#' camera and/or microphone should be used.
#'
#' By default on most phones, this will accept still photos or video. For
#' still photos only, also use `accept="image/*"`. For video only, use
#' `accept="video/*"`.
#' @examples #' @examples
#' ## Only run examples in interactive R sessions #' ## Only run examples in interactive R sessions
#' if (interactive()) { #' if (interactive()) {
@@ -72,9 +56,7 @@
#' } #' }
#' #'
#' @section Server value: #' @section Server value:
#' #' A `data.frame` that contains one row for each selected file, and following columns:
#' A `data.frame` that contains one row for each selected file, and following
#' columns:
#' \describe{ #' \describe{
#' \item{`name`}{The filename provided by the web browser. This is #' \item{`name`}{The filename provided by the web browser. This is
#' **not** the path to read to get at the actual data that was uploaded #' **not** the path to read to get at the actual data that was uploaded
@@ -91,8 +73,7 @@
#' #'
#' @export #' @export
fileInput <- function(inputId, label, multiple = FALSE, accept = NULL, fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
width = NULL, buttonLabel = "Browse...", placeholder = "No file selected", width = NULL, buttonLabel = "Browse...", placeholder = "No file selected") {
capture = NULL) {
restoredValue <- restoreInput(id = inputId, default = NULL) restoredValue <- restoreInput(id = inputId, default = NULL)
@@ -108,7 +89,6 @@ fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
inputTag <- tags$input( inputTag <- tags$input(
id = inputId, id = inputId,
class = "shiny-input-file",
name = inputId, name = inputId,
type = "file", type = "file",
# Don't use "display: none;" style, which causes keyboard accessibility issue; instead use the following workaround: https://css-tricks.com/places-its-tempting-to-use-display-none-but-dont/ # Don't use "display: none;" style, which causes keyboard accessibility issue; instead use the following workaround: https://css-tricks.com/places-its-tempting-to-use-display-none-but-dont/
@@ -121,9 +101,6 @@ fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
if (length(accept) > 0) if (length(accept) > 0)
inputTag$attribs$accept <- paste(accept, collapse=',') inputTag$attribs$accept <- paste(accept, collapse=',')
if (!is.null(capture)) {
inputTag$attribs$capture <- capture
}
div(class = "form-group shiny-input-container", div(class = "form-group shiny-input-container",
style = css(width = validateCssUnit(width)), style = css(width = validateCssUnit(width)),

View File

@@ -29,36 +29,22 @@
#' A numeric vector of length 1. #' A numeric vector of length 1.
#' #'
#' @export #' @export
numericInput <- function( numericInput <- function(inputId, label, value, min = NA, max = NA, step = NA,
inputId, width = NULL) {
label,
value,
min = NA,
max = NA,
step = NA,
width = NULL,
...,
updateOn = c("change", "blur")
) {
rlang::check_dots_empty()
updateOn <- rlang::arg_match(updateOn)
value <- restoreInput(id = inputId, default = value) value <- restoreInput(id = inputId, default = value)
# build input tag # build input tag
inputTag <- tags$input( inputTag <- tags$input(id = inputId, type = "number", class="form-control",
id = inputId, value = formatNoSci(value))
type = "number", if (!is.na(min))
class = "shiny-input-number form-control", inputTag$attribs$min = min
value = formatNoSci(value), if (!is.na(max))
`data-update-on` = updateOn inputTag$attribs$max = max
) if (!is.na(step))
if (!is.na(min)) inputTag$attribs$min = min inputTag$attribs$step = step
if (!is.na(max)) inputTag$attribs$max = max
if (!is.na(step)) inputTag$attribs$step = step
div( div(class = "form-group shiny-input-container",
class = "form-group shiny-input-container",
style = css(width = validateCssUnit(width)), style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label), shinyInputLabel(inputId, label),
inputTag inputTag

View File

@@ -30,29 +30,12 @@
#' shinyApp(ui, server) #' shinyApp(ui, server)
#' } #' }
#' @export #' @export
passwordInput <- function( passwordInput <- function(inputId, label, value = "", width = NULL,
inputId, placeholder = NULL) {
label, div(class = "form-group shiny-input-container",
value = "",
width = NULL,
placeholder = NULL,
...,
updateOn = c("change", "blur")
) {
rlang::check_dots_empty()
updateOn <- rlang::arg_match(updateOn)
div(
class = "form-group shiny-input-container",
style = css(width = validateCssUnit(width)), style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label), shinyInputLabel(inputId, label),
tags$input( tags$input(id = inputId, type="password", class="form-control", value=value,
id = inputId, placeholder = placeholder)
type = "password",
class = "shiny-input-password form-control",
value = value,
placeholder = placeholder,
`data-update-on` = updateOn
)
) )
} }

View File

@@ -4,7 +4,7 @@
#' from a list of values. #' from a list of values.
#' #'
#' By default, `selectInput()` and `selectizeInput()` use the JavaScript library #' By default, `selectInput()` and `selectizeInput()` use the JavaScript library
#' \pkg{selectize.js} (<https://selectize.dev/>) instead of #' \pkg{selectize.js} (<https://github.com/selectize/selectize.js>) instead of
#' the basic select input element. To use the standard HTML select input #' the basic select input element. To use the standard HTML select input
#' element, use `selectInput()` with `selectize=FALSE`. #' element, use `selectInput()` with `selectize=FALSE`.
#' #'
@@ -106,7 +106,6 @@ selectInput <- function(inputId, label, choices, selected = NULL,
# create select tag and add options # create select tag and add options
selectTag <- tags$select( selectTag <- tags$select(
id = inputId, id = inputId,
class = "shiny-input-select",
class = if (!selectize) "form-control", class = if (!selectize) "form-control",
size = size, size = size,
selectOptions(choices, selected, inputId, selectize) selectOptions(choices, selected, inputId, selectize)
@@ -173,7 +172,7 @@ needOptgroup <- function(choices) {
#' @rdname selectInput #' @rdname selectInput
#' @param ... Arguments passed to `selectInput()`. #' @param ... Arguments passed to `selectInput()`.
#' @param options A list of options. See the documentation of \pkg{selectize.js}(<https://selectize.dev/docs/usage>) #' @param options A list of options. See the documentation of \pkg{selectize.js}
#' for possible options (character option values inside [base::I()] will #' for possible options (character option values inside [base::I()] will
#' be treated as literal JavaScript code; see [renderDataTable()] #' be treated as literal JavaScript code; see [renderDataTable()]
#' for details). #' for details).
@@ -214,7 +213,14 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
deps <- list(selectizeDependency()) deps <- list(selectizeDependency())
if ('drag_drop' %in% options$plugins) { if ('drag_drop' %in% options$plugins) {
deps[[length(deps) + 1]] <- jqueryuiDependency() deps <- c(
deps,
list(htmlDependency(
'jqueryui', '1.12.1',
c(href = 'shared/jqueryui'),
script = 'jquery-ui.min.js'
))
)
} }
# Insert script on same level as <select> tag # Insert script on same level as <select> tag
@@ -237,12 +243,20 @@ selectizeDependency <- function() {
} }
selectizeDependencyFunc <- function(theme) { selectizeDependencyFunc <- function(theme) {
selectizeVersion <- "0.12.4"
if (!is_bs_theme(theme)) { if (!is_bs_theme(theme)) {
return(selectizeStaticDependency(version_selectize)) return(selectizeStaticDependency(selectizeVersion))
} }
bs_version <- bslib::theme_version(theme) selectizeDir <- system.file(package = "shiny", "www/shared/selectize/")
stylesheet <- file.path(
selectizeDir, "scss",
if ("3" %in% bslib::theme_version(theme)) {
"selectize.bootstrap3.scss"
} else {
"selectize.bootstrap4.scss"
}
)
# It'd be cleaner to ship the JS in a separate, href-based, # It'd be cleaner to ship the JS in a separate, href-based,
# HTML dependency (which we currently do for other themable widgets), # HTML dependency (which we currently do for other themable widgets),
# but DT, crosstalk, and maybe other pkgs include selectize JS/CSS # but DT, crosstalk, and maybe other pkgs include selectize JS/CSS
@@ -250,46 +264,28 @@ selectizeDependencyFunc <- function(theme) {
# name, the JS/CSS would be loaded/included twice, which leads to # name, the JS/CSS would be loaded/included twice, which leads to
# strange issues, especially since we now include a 3rd party # strange issues, especially since we now include a 3rd party
# accessibility plugin https://github.com/rstudio/shiny/pull/3153 # accessibility plugin https://github.com/rstudio/shiny/pull/3153
selectizeDir <- system_file(package = "shiny", "www/shared/selectize/") script <- file.path(
script <- file.path(selectizeDir, selectizeScripts()) selectizeDir, c("js/selectize.min.js", "accessibility/js/selectize-plugin-a11y.min.js")
)
bslib::bs_dependency( bslib::bs_dependency(
input = selectizeSass(bs_version), input = sass::sass_file(stylesheet),
theme = theme, theme = theme,
name = "selectize", name = "selectize",
version = version_selectize, version = selectizeVersion,
cache_key_extra = get_package_version("shiny"), cache_key_extra = shinyPackageVersion(),
.dep_args = list(script = script) .dep_args = list(script = script)
) )
} }
selectizeSass <- function(bs_version) {
selectizeDir <- system_file(package = "shiny", "www/shared/selectize/")
stylesheet <- file.path(
selectizeDir, "scss", paste0("selectize.bootstrap", bs_version, ".scss")
)
sass::sass_file(stylesheet)
}
selectizeStaticDependency <- function(version) { selectizeStaticDependency <- function(version) {
htmlDependency( htmlDependency(
"selectize", "selectize", version,
version, src = c(href = "shared/selectize"),
src = "www/shared/selectize",
package = "shiny",
stylesheet = "css/selectize.bootstrap3.css", stylesheet = "css/selectize.bootstrap3.css",
script = selectizeScripts() script = c(
) "js/selectize.min.js",
} "accessibility/js/selectize-plugin-a11y.min.js"
)
selectizeScripts <- function() {
isMinified <- isTRUE(get_devmode_option("shiny.minified", TRUE))
paste0(
c(
"js/selectize",
"accessibility/js/selectize-plugin-a11y"
),
if (isMinified) ".min.js" else ".js"
) )
} }
@@ -301,7 +297,7 @@ selectizeScripts <- function() {
#' #'
#' By default, `varSelectInput()` and `selectizeInput()` use the #' By default, `varSelectInput()` and `selectizeInput()` use the
#' JavaScript library \pkg{selectize.js} #' JavaScript library \pkg{selectize.js}
#' (<https://selectize.dev/>) to instead of the basic #' (<https://github.com/selectize/selectize.js>) to instead of the basic
#' select input element. To use the standard HTML select input element, use #' select input element. To use the standard HTML select input element, use
#' `selectInput()` with `selectize=FALSE`. #' `selectInput()` with `selectize=FALSE`.
#' #'
@@ -397,7 +393,7 @@ varSelectInput <- function(
#' @rdname varSelectInput #' @rdname varSelectInput
#' @param ... Arguments passed to `varSelectInput()`. #' @param ... Arguments passed to `varSelectInput()`.
#' @param options A list of options. See the documentation of \pkg{selectize.js}(<https://selectize.dev/docs/usage>) #' @param options A list of options. See the documentation of \pkg{selectize.js}
#' for possible options (character option values inside [base::I()] will #' for possible options (character option values inside [base::I()] will
#' be treated as literal JavaScript code; see [renderDataTable()] #' be treated as literal JavaScript code; see [renderDataTable()]
#' for details). #' for details).

View File

@@ -201,53 +201,59 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
} }
ionRangeSliderVersion <- "2.3.1"
ionRangeSliderDependency <- function() { ionRangeSliderDependency <- function() {
list( list(
# ion.rangeSlider also needs normalize.css, which is already included in Bootstrap. # ion.rangeSlider also needs normalize.css, which is already included in Bootstrap.
htmlDependency( htmlDependency(
"ionrangeslider-javascript", "ionrangeslider-javascript", ionRangeSliderVersion,
version_ion_range_slider, src = c(href = "shared/ionrangeslider"),
src = "www/shared/ionrangeslider",
package = "shiny",
script = "js/ion.rangeSlider.min.js" script = "js/ion.rangeSlider.min.js"
), ),
htmlDependency( htmlDependency(
"strftime", "strftime", "0.9.2",
version_strftime, src = c(href = "shared/strftime"),
src = "www/shared/strftime",
package = "shiny",
script = "strftime-min.js" script = "strftime-min.js"
), ),
bslib::bs_dependency_defer(ionRangeSliderDependencyCSS) bslib::bs_dependency_defer(ionRangeSliderDependencyCSS)
) )
} }
ionRangeSliderDependencySass <- function() {
list(
list(accent = "$component-active-bg"),
sass::sass_file(
system_file(package = "shiny", "www/shared/ionrangeslider/scss/shiny.scss")
)
)
}
ionRangeSliderDependencyCSS <- function(theme) { ionRangeSliderDependencyCSS <- function(theme) {
if (!is_bs_theme(theme)) { if (!is_bs_theme(theme)) {
return(htmlDependency( return(htmlDependency(
"ionrangeslider-css", "ionrangeslider-css",
version_ion_range_slider, ionRangeSliderVersion,
src = "www/shared/ionrangeslider", src = c(href = "shared/ionrangeslider"),
package = "shiny",
stylesheet = "css/ion.rangeSlider.css" stylesheet = "css/ion.rangeSlider.css"
)) ))
} }
# Remap some variable names for ionRangeSlider's scss
sass_input <- list(
list(
# The bootswatch materia theme sets $input-bg: transparent;
# which is an issue for the slider's handle(s) (#3130)
bg = "if(alpha($input-bg)==0, $body-bg, $input-bg)",
fg = sprintf(
"if(alpha($input-color)==0, $%s, $input-color)",
if ("3" %in% bslib::theme_version(theme)) "text-color" else "body-color"
),
accent = "$component-active-bg",
`font-family` = "$font-family-base"
),
sass::sass_file(
system.file(package = "shiny", "www/shared/ionrangeslider/scss/shiny.scss")
)
)
bslib::bs_dependency( bslib::bs_dependency(
input = ionRangeSliderDependencySass(), input = sass_input,
theme = theme, theme = theme,
name = "ionRangeSlider", name = "ionRangeSlider",
version = version_ion_range_slider, version = ionRangeSliderVersion,
cache_key_extra = get_package_version("shiny") cache_key_extra = shinyPackageVersion()
) )
} }

View File

@@ -57,7 +57,7 @@ submitButton <- function(text = "Apply Changes", icon = NULL, width = NULL) {
div( div(
tags$button( tags$button(
type="submit", type="submit",
class="btn btn-primary shiny-submit-button", class="btn btn-primary",
style = css(width = validateCssUnit(width)), style = css(width = validateCssUnit(width)),
list(icon, text) list(icon, text)
) )

View File

@@ -10,14 +10,6 @@
#' @param placeholder A character string giving the user a hint as to what can #' @param 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 #' be entered into the control. Internet Explorer 8 and 9 do not support this
#' option. #' option.
#' @param ... Ignored, included to require named arguments and for future
#' feature expansion.
#' @param updateOn A character vector specifying when the input should be
#' updated. Options are `"change"` (default) and `"blur"`. Use `"change"` to
#' update the input immediately whenever the value changes. Use `"blur"`to
#' delay the input update until the input loses focus (the user moves away
#' from the input), or when Enter is pressed (or Cmd/Ctrl + Enter for
#' [textAreaInput()]).
#' @return A text input control that can be added to a UI definition. #' @return A text input control that can be added to a UI definition.
#' #'
#' @family input elements #' @family input elements
@@ -42,31 +34,15 @@
#' unless `value` is provided. #' unless `value` is provided.
#' #'
#' @export #' @export
textInput <- function( textInput <- function(inputId, label, value = "", width = NULL,
inputId, placeholder = NULL) {
label,
value = "",
width = NULL,
placeholder = NULL,
...,
updateOn = c("change", "blur")
) {
rlang::check_dots_empty()
updateOn <- rlang::arg_match(updateOn)
value <- restoreInput(id = inputId, default = value) value <- restoreInput(id = inputId, default = value)
div( div(class = "form-group shiny-input-container",
class = "form-group shiny-input-container",
style = css(width = validateCssUnit(width)), style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label), shinyInputLabel(inputId, label),
tags$input( tags$input(id = inputId, type="text", class="form-control", value=value,
id = inputId, placeholder = placeholder)
type = "text",
class = "shiny-input-text form-control",
value = value,
placeholder = placeholder,
`data-update-on` = updateOn
)
) )
} }

View File

@@ -16,8 +16,6 @@
#' @param resize Which directions the textarea box can be resized. Can be one of #' @param resize Which directions the textarea box can be resized. Can be one of
#' `"both"`, `"none"`, `"vertical"`, and `"horizontal"`. The default, `NULL`, #' `"both"`, `"none"`, `"vertical"`, and `"horizontal"`. The default, `NULL`,
#' will use the client browser's default setting for resizing textareas. #' will use the client browser's default setting for resizing textareas.
#' @param autoresize If `TRUE`, the textarea will automatically resize to fit
#' the input text.
#' @return A textarea input control that can be added to a UI definition. #' @return A textarea input control that can be added to a UI definition.
#' #'
#' @family input elements #' @family input elements
@@ -43,22 +41,8 @@
#' unless `value` is provided. #' unless `value` is provided.
#' #'
#' @export #' @export
textAreaInput <- function( textAreaInput <- function(inputId, label, value = "", width = NULL, height = NULL,
inputId, cols = NULL, rows = NULL, placeholder = NULL, resize = NULL) {
label,
value = "",
width = NULL,
height = NULL,
cols = NULL,
rows = NULL,
placeholder = NULL,
resize = NULL,
...,
autoresize = FALSE,
updateOn = c("change", "blur")
) {
rlang::check_dots_empty()
updateOn <- rlang::arg_match(updateOn)
value <- restoreInput(id = inputId, default = value) value <- restoreInput(id = inputId, default = value)
@@ -66,30 +50,23 @@ textAreaInput <- function(
resize <- match.arg(resize, c("both", "none", "vertical", "horizontal")) resize <- match.arg(resize, c("both", "none", "vertical", "horizontal"))
} }
classes <- "form-control" style <- css(
if (autoresize) { # The width is specified on the parent div.
classes <- c(classes, "textarea-autoresize") width = if (!is.null(width)) "width: 100%;",
if (is.null(rows)) { height = validateCssUnit(height),
rows <- 1 resize = resize
} )
}
div( div(class = "form-group shiny-input-container",
class = "shiny-input-textarea form-group shiny-input-container",
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label), shinyInputLabel(inputId, label),
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
tags$textarea( tags$textarea(
id = inputId, id = inputId,
class = classes, class = "form-control",
placeholder = placeholder, placeholder = placeholder,
style = css( style = style,
width = if (!is.null(width)) "100%",
height = validateCssUnit(height),
resize = resize
),
rows = rows, rows = rows,
cols = cols, cols = cols,
`data-update-on` = updateOn,
value value
) )
) )

View File

@@ -41,7 +41,7 @@ normalizeChoicesArgs <- function(choices, choiceNames, choiceValues,
if (length(choiceNames) != length(choiceValues)) { if (length(choiceNames) != length(choiceValues)) {
stop("`choiceNames` and `choiceValues` must have the same length.") stop("`choiceNames` and `choiceValues` must have the same length.")
} }
if (any_named(choiceNames) || any_named(choiceValues)) { if (anyNamed(choiceNames) || anyNamed(choiceValues)) {
stop("`choiceNames` and `choiceValues` must not be named.") stop("`choiceNames` and `choiceValues` must not be named.")
} }
} else { } else {

View File

@@ -112,13 +112,35 @@
#' #'
#' } #' }
#' @export #' @export
insertTab <- function(inputId, tab, target = NULL, insertTab <- function(inputId, tab, target,
position = c("after", "before"), select = FALSE, position = c("before", "after"), select = FALSE,
session = getDefaultReactiveDomain()) { session = getDefaultReactiveDomain()) {
bslib::nav_insert( force(target)
inputId, tab, target, force(select)
match.arg(position), select, session position <- match.arg(position)
) inputId <- session$ns(inputId)
# Barbara -- August 2017
# Note: until now, the number of tabs in a tabsetPanel (or navbarPage
# or navlistPanel) was always fixed. So, an easy way to give an id to
# a tab was simply incrementing a counter. (Just like it was easy to
# give a random 4-digit number to identify the tabsetPanel). Since we
# can only know this in the client side, we'll just pass `id` and
# `tsid` (TabSetID) as dummy values that will be fixed in the JS code.
item <- buildTabItem("id", "tsid", TRUE, divTag = tab,
textFilter = if (is.character(tab)) navbarMenuTextFilter else NULL)
callback <- function() {
session$sendInsertTab(
inputId = inputId,
liTag = processDeps(item$liTag, session),
divTag = processDeps(item$divTag, session),
menuName = NULL,
target = target,
position = position,
select = select)
}
session$onFlush(callback, once = TRUE)
} }
#' @param menuName This argument should only be used when you want to #' @param menuName This argument should only be used when you want to
@@ -137,21 +159,63 @@ insertTab <- function(inputId, tab, target = NULL,
#' @export #' @export
prependTab <- function(inputId, tab, select = FALSE, menuName = NULL, prependTab <- function(inputId, tab, select = FALSE, menuName = NULL,
session = getDefaultReactiveDomain()) { session = getDefaultReactiveDomain()) {
bslib::nav_prepend(inputId, tab, menu_title = menuName, select = select, session = session) force(select)
force(menuName)
inputId <- session$ns(inputId)
item <- buildTabItem("id", "tsid", TRUE, divTag = tab,
textFilter = if (is.character(tab)) navbarMenuTextFilter else NULL)
callback <- function() {
session$sendInsertTab(
inputId = inputId,
liTag = processDeps(item$liTag, session),
divTag = processDeps(item$divTag, session),
menuName = menuName,
target = NULL,
position = "after",
select = select)
}
session$onFlush(callback, once = TRUE)
} }
#' @rdname insertTab #' @rdname insertTab
#' @export #' @export
appendTab <- function(inputId, tab, select = FALSE, menuName = NULL, appendTab <- function(inputId, tab, select = FALSE, menuName = NULL,
session = getDefaultReactiveDomain()) { session = getDefaultReactiveDomain()) {
bslib::nav_append(inputId, tab, menu_title = menuName, select = select, session = session) force(select)
force(menuName)
inputId <- session$ns(inputId)
item <- buildTabItem("id", "tsid", TRUE, divTag = tab,
textFilter = if (is.character(tab)) navbarMenuTextFilter else NULL)
callback <- function() {
session$sendInsertTab(
inputId = inputId,
liTag = processDeps(item$liTag, session),
divTag = processDeps(item$divTag, session),
menuName = menuName,
target = NULL,
position = "before",
select = select)
}
session$onFlush(callback, once = TRUE)
} }
#' @rdname insertTab #' @rdname insertTab
#' @export #' @export
removeTab <- function(inputId, target, removeTab <- function(inputId, target,
session = getDefaultReactiveDomain()) { session = getDefaultReactiveDomain()) {
bslib::nav_remove(inputId, target, session) force(target)
inputId <- session$ns(inputId)
callback <- function() {
session$sendRemoveTab(
inputId = inputId,
target = target)
}
session$onFlush(callback, once = TRUE)
} }

View File

@@ -1,6 +1,6 @@
#' Insert and remove UI objects #' Insert and remove UI objects
#' #'
#' These functions allow you to dynamically add and remove arbitrary UI #' These functions allow you to dynamically add and remove arbirary UI
#' into your app, whenever you want, as many times as you want. #' into your app, whenever you want, as many times as you want.
#' Unlike [renderUI()], the UI generated with `insertUI()` is persistent: #' Unlike [renderUI()], the UI generated with `insertUI()` is persistent:
#' once it's created, it stays there until removed by `removeUI()`. Each #' once it's created, it stays there until removed by `removeUI()`. Each
@@ -11,7 +11,7 @@
#' function. #' function.
#' #'
#' It's particularly useful to pair `removeUI` with `insertUI()`, but there is #' It's particularly useful to pair `removeUI` with `insertUI()`, but there is
#' no restriction on what you can use it on. Any element that can be selected #' no restriction on what you can use on. Any element that can be selected
#' through a jQuery selector can be removed through this function. #' through a jQuery selector can be removed through this function.
#' #'
#' @param selector A string that is accepted by jQuery's selector #' @param selector A string that is accepted by jQuery's selector

View File

@@ -76,20 +76,16 @@ absolutePanel <- function(...,
style <- paste(paste(names(cssProps), cssProps, sep = ':', collapse = ';'), ';', sep='') style <- paste(paste(names(cssProps), cssProps, sep = ':', collapse = ';'), ';', sep='')
divTag <- tags$div(style=style, ...) divTag <- tags$div(style=style, ...)
if (isTRUE(draggable)) {
if (identical(draggable, FALSE)) { divTag <- tagAppendAttributes(divTag, class='draggable')
return(tagList(
singleton(tags$head(tags$script(src='shared/jqueryui/jquery-ui.min.js'))),
divTag,
tags$script('$(".draggable").draggable();')
))
} else {
return(divTag) return(divTag)
} }
# Add Shiny inputs and htmlwidgets to 'non-draggable' elements
# Cf. https://api.jqueryui.com/draggable/#option-cancel
dragOpts <- '{cancel: ".shiny-input-container,.html-widget,input,textarea,button,select,option"}'
dragJS <- sprintf('$(".draggable").draggable(%s);', dragOpts)
tagList(
tagAppendAttributes(divTag, class='draggable'),
jqueryuiDependency(),
tags$script(HTML(dragJS))
)
} }
#' @rdname absolutePanel #' @rdname absolutePanel
@@ -103,14 +99,3 @@ fixedPanel <- function(...,
width=width, height=height, draggable=draggable, cursor=match.arg(cursor), width=width, height=height, draggable=draggable, cursor=match.arg(cursor),
fixed=TRUE) fixed=TRUE)
} }
jqueryuiDependency <- function() {
htmlDependency(
"jqueryui",
version_jqueryui,
src = "www/shared/jqueryui",
package = "shiny",
script = "jquery-ui.min.js"
)
}

View File

@@ -4,7 +4,6 @@
#' themselves in knitr/rmarkdown documents. #' themselves in knitr/rmarkdown documents.
#' #'
#' @name knitr_methods #' @name knitr_methods
#' @keywords internal
#' @param x Object to knit_print #' @param x Object to knit_print
#' @param ... Additional knit_print arguments #' @param ... Additional knit_print arguments
NULL NULL
@@ -63,7 +62,7 @@ knit_print.shiny.appobj <- function(x, ...) {
#' @param inline Whether the object is printed inline. #' @param inline Whether the object is printed inline.
knit_print.shiny.render.function <- function(x, ..., inline = FALSE) { knit_print.shiny.render.function <- function(x, ..., inline = FALSE) {
x <- htmltools::as.tags(x, inline = inline) x <- htmltools::as.tags(x, inline = inline)
output <- knitr::knit_print(tagList(x), ..., inline = inline) output <- knitr::knit_print(tagList(x))
attr(output, "knit_cacheable") <- FALSE attr(output, "knit_cacheable") <- FALSE
attr(output, "knit_meta") <- append(attr(output, "knit_meta"), attr(output, "knit_meta") <- append(attr(output, "knit_meta"),
shiny_rmd_warning()) shiny_rmd_warning())
@@ -77,5 +76,5 @@ knit_print.reactive <- function(x, ..., inline = FALSE) {
renderFunc <- if (inline) renderText else renderPrint renderFunc <- if (inline) renderText else renderPrint
knitr::knit_print(renderFunc({ knitr::knit_print(renderFunc({
x() x()
}), ..., inline = inline) }), inline = inline)
} }

12
R/map.R
View File

@@ -1,3 +1,4 @@
#' @importFrom fastmap fastmap
Map <- R6Class( Map <- R6Class(
'Map', 'Map',
portable = FALSE, portable = FALSE,
@@ -48,12 +49,9 @@ Map <- R6Class(
) )
) )
#' @export as.list.Map <- function(map) {
as.list.Map <- function(x, ...) { map$values()
x$values()
} }
length.Map <- function(map) {
#' @export map$size()
length.Map <- function(x) {
x$size()
} }

View File

@@ -348,7 +348,7 @@ HandlerManager <- R6Class("HandlerManager",
httpResponse(status = 500L, httpResponse(status = 500L,
content_type = "text/html; charset=UTF-8", content_type = "text/html; charset=UTF-8",
content = as.character(htmltools::htmlTemplate( content = as.character(htmltools::htmlTemplate(
system_file("template", "error.html", package = "shiny"), system.file("template", "error.html", package = "shiny"),
message = conditionMessage(err) message = conditionMessage(err)
)) ))
) )

View File

@@ -1,5 +1,5 @@
# Promise helpers taken from: # Promise helpers taken from:
# https://github.com/rstudio/promises/blob/main/tests/testthat/common.R # https://github.com/rstudio/promises/blob/master/tests/testthat/common.R
# Block until all pending later tasks have executed # Block until all pending later tasks have executed
wait_for_it <- function() { wait_for_it <- function() {
while (!later::loop_empty()) { while (!later::loop_empty()) {
@@ -457,11 +457,6 @@ MockShinySession <- R6Class(
function(v){ function(v){
list(val = v, err = NULL) list(val = v, err = NULL)
}, catch=function(e){ }, catch=function(e){
if (
!inherits(e, c("shiny.custom.error", "shiny.output.cancel", "shiny.output.progress", "shiny.silent.error"))
) {
self$unhandledError(e, close = FALSE)
}
list(val = NULL, err = e) list(val = NULL, err = e)
}) })
}) })
@@ -565,26 +560,10 @@ MockShinySession <- R6Class(
rootScope = function() { rootScope = function() {
self self
}, },
#' @description Add an unhandled error callback.
#' @param callback The callback to add, which should accept an error object
#' as its first argument.
#' @return A deregistration function.
onUnhandledError = function(callback) {
private$unhandledErrorCallbacks$register(callback)
},
#' @description Called by observers when a reactive expression errors. #' @description Called by observers when a reactive expression errors.
#' @param e An error object. #' @param e An error object.
#' @param close If `TRUE`, the session will be closed after the error is unhandledError = function(e) {
#' handled, defaults to `FALSE`. self$close()
unhandledError = function(e, close = TRUE) {
if (close) {
class(e) <- c("shiny.error.fatal", class(e))
}
private$unhandledErrorCallbacks$invoke(e, onError = printError)
.globals$onUnhandledErrorCallbacks$invoke(e, onError = printError)
if (close) self$close()
}, },
#' @description Freeze a value until the flush cycle completes. #' @description Freeze a value until the flush cycle completes.
#' @param x A `ReactiveValues` object. #' @param x A `ReactiveValues` object.
@@ -641,9 +620,6 @@ MockShinySession <- R6Class(
flushedCBs = NULL, flushedCBs = NULL,
# @field endedCBs `Callbacks` called when session ends. # @field endedCBs `Callbacks` called when session ends.
endedCBs = NULL, endedCBs = NULL,
# @field unhandledErrorCallbacks `Callbacks` called when an unhandled error
# occurs.
unhandledErrorCallbacks = Callbacks$new(),
# @field timer `MockableTimerCallbacks` called at particular times. # @field timer `MockableTimerCallbacks` called at particular times.
timer = NULL, timer = NULL,
# @field was_closed Set to `TRUE` once the session is closed. # @field was_closed Set to `TRUE` once the session is closed.

View File

@@ -43,10 +43,7 @@ removeModal <- function(session = getDefaultReactiveDomain()) {
#' @param title An optional title for the dialog. #' @param title An optional title for the dialog.
#' @param footer UI for footer. Use `NULL` for no footer. #' @param footer UI for footer. Use `NULL` for no footer.
#' @param size One of `"s"` for small, `"m"` (the default) for medium, #' @param size One of `"s"` for small, `"m"` (the default) for medium,
#' `"l"` for large, or `"xl"` for extra large. Note that `"xl"` only #' or `"l"` for large.
#' works with Bootstrap 4 and above (to opt-in to Bootstrap 4+,
#' pass [bslib::bs_theme()] to the `theme` argument of a page container
#' like [fluidPage()]).
#' @param easyClose If `TRUE`, the modal dialog can be dismissed by #' @param easyClose If `TRUE`, the modal dialog can be dismissed by
#' clicking outside the dialog box, or be pressing the Escape key. If #' clicking outside the dialog box, or be pressing the Escape key. If
#' `FALSE` (the default), the modal dialog can't be dismissed in those #' `FALSE` (the default), the modal dialog can't be dismissed in those
@@ -154,25 +151,18 @@ removeModal <- function(session = getDefaultReactiveDomain()) {
#' } #' }
#' @export #' @export
modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"), modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"),
size = c("m", "s", "l", "xl"), easyClose = FALSE, fade = TRUE) { size = c("m", "s", "l"), easyClose = FALSE, fade = TRUE) {
size <- match.arg(size) size <- match.arg(size)
backdrop <- if (!easyClose) "static" cls <- if (fade) "modal fade" else "modal"
keyboard <- if (!easyClose) "false" div(id = "shiny-modal", class = cls, tabindex = "-1",
div( `data-backdrop` = if (!easyClose) "static",
id = "shiny-modal", `data-keyboard` = if (!easyClose) "false",
class = "modal",
class = if (fade) "fade",
tabindex = "-1",
`data-backdrop` = backdrop,
`data-bs-backdrop` = backdrop,
`data-keyboard` = keyboard,
`data-bs-keyboard` = keyboard,
div( div(
class = "modal-dialog", class = "modal-dialog",
class = switch(size, s = "modal-sm", m = NULL, l = "modal-lg", xl = "modal-xl"), class = switch(size, s = "modal-sm", m = NULL, l = "modal-lg"),
div(class = "modal-content", div(class = "modal-content",
if (!is.null(title)) div(class = "modal-header", if (!is.null(title)) div(class = "modal-header",
tags$h4(class = "modal-title", title) tags$h4(class = "modal-title", title)
@@ -181,26 +171,14 @@ modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"),
if (!is.null(footer)) div(class = "modal-footer", footer) if (!is.null(footer)) div(class = "modal-footer", footer)
) )
), ),
# jQuery plugin doesn't work in Bootstrap 5, but vanilla JS doesn't work in Bootstrap 4 :sob: tags$script("$('#shiny-modal').modal().focus();")
tags$script(HTML(
"if (window.bootstrap && !window.bootstrap.Modal.VERSION.match(/^4\\./)) {
var modal = new bootstrap.Modal(document.getElementById('shiny-modal'));
modal.show();
} else {
$('#shiny-modal').modal().focus();
}"
))
) )
} }
#' @export #' @export
#' @rdname modalDialog #' @rdname modalDialog
modalButton <- function(label, icon = NULL) { modalButton <- function(label, icon = NULL) {
tags$button( tags$button(type = "button", class = "btn btn-default",
type = "button", `data-dismiss` = "modal", validateIcon(icon), label
class = "btn btn-default",
`data-dismiss` = "modal",
`data-bs-dismiss` = "modal",
validateIcon(icon), label
) )
} }

View File

@@ -53,12 +53,10 @@ Context <- R6Class(
promises::with_promise_domain(reactivePromiseDomain(), { promises::with_promise_domain(reactivePromiseDomain(), {
withReactiveDomain(.domain, { withReactiveDomain(.domain, {
captureStackTraces({ env <- .getReactiveEnvironment()
env <- .getReactiveEnvironment() rLog$enter(.reactId, id, .reactType, .domain)
rLog$enter(.reactId, id, .reactType, .domain) on.exit(rLog$exit(.reactId, id, .reactType, .domain), add = TRUE)
on.exit(rLog$exit(.reactId, id, .reactType, .domain), add = TRUE) env$runWith(self, func)
env$runWith(self, func)
})
}) })
}) })
}, },
@@ -221,11 +219,13 @@ getDummyContext <- function() {
wrapForContext <- function(func, ctx) { wrapForContext <- function(func, ctx) {
force(func) force(func)
force(ctx) # may be NULL (in the case of maskReactiveContext()) force(ctx)
function(...) { function(...) {
.getReactiveEnvironment()$runWith(ctx, function() { ctx$run(function() {
func(...) captureStackTraces(
func(...)
)
}) })
} }
} }
@@ -234,18 +234,12 @@ reactivePromiseDomain <- function() {
promises::new_promise_domain( promises::new_promise_domain(
wrapOnFulfilled = function(onFulfilled) { wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled) force(onFulfilled)
ctx <- getCurrentContext()
# ctx will be NULL if we're in a maskReactiveContext()
ctx <- if (hasCurrentContext()) getCurrentContext() else NULL
wrapForContext(onFulfilled, ctx) wrapForContext(onFulfilled, ctx)
}, },
wrapOnRejected = function(onRejected) { wrapOnRejected = function(onRejected) {
force(onRejected) force(onRejected)
ctx <- getCurrentContext()
# ctx will be NULL if we're in a maskReactiveContext()
ctx <- if (hasCurrentContext()) getCurrentContext() else NULL
wrapForContext(onRejected, ctx) wrapForContext(onRejected, ctx)
} }
) )

View File

@@ -326,9 +326,6 @@ ReactiveValues <- R6Class(
.dedupe = logical(0), .dedupe = logical(0),
# Key, asList(), or names() have been retrieved # Key, asList(), or names() have been retrieved
.hasRetrieved = list(), .hasRetrieved = list(),
# All names, in insertion order. The names are also stored in the .values
# object, but it does not preserve order.
.nameOrder = character(0),
initialize = function( initialize = function(
@@ -406,11 +403,6 @@ ReactiveValues <- R6Class(
return(invisible()) return(invisible())
} }
# If it's new, append key to the name order
if (!key_exists) {
.nameOrder[length(.nameOrder) + 1] <<- key
}
# set the value for better logging # set the value for better logging
.values$set(key, value) .values$set(key, value)
@@ -452,13 +444,14 @@ ReactiveValues <- R6Class(
}, },
names = function() { names = function() {
nameValues <- .values$keys()
if (!isTRUE(.hasRetrieved$names)) { if (!isTRUE(.hasRetrieved$names)) {
domain <- getDefaultReactiveDomain() domain <- getDefaultReactiveDomain()
rLog$defineNames(.reactId, .nameOrder, .label, domain) rLog$defineNames(.reactId, nameValues, .label, domain)
.hasRetrieved$names <<- TRUE .hasRetrieved$names <<- TRUE
} }
.namesDeps$register() .namesDeps$register()
return(.nameOrder) return(nameValues)
}, },
# Get a metadata value. Does not trigger reactivity. # Get a metadata value. Does not trigger reactivity.
@@ -506,7 +499,7 @@ ReactiveValues <- R6Class(
}, },
toList = function(all.names=FALSE) { toList = function(all.names=FALSE) {
listValue <- .values$mget(.nameOrder) listValue <- .values$values()
if (!all.names) { if (!all.names) {
listValue <- listValue[!grepl("^\\.", base::names(listValue))] listValue <- listValue[!grepl("^\\.", base::names(listValue))]
} }
@@ -575,7 +568,7 @@ ReactiveValues <- R6Class(
#' @seealso [isolate()] and [is.reactivevalues()]. #' @seealso [isolate()] and [is.reactivevalues()].
#' @export #' @export
reactiveValues <- function(...) { reactiveValues <- function(...) {
args <- list2(...) args <- list(...)
if ((length(args) > 0) && (is.null(names(args)) || any(names(args) == ""))) if ((length(args) > 0) && (is.null(names(args)) || any(names(args) == "")))
rlang::abort("All arguments passed to reactiveValues() must be named.") rlang::abort("All arguments passed to reactiveValues() must be named.")
@@ -882,7 +875,8 @@ Observable <- R6Class(
invisible(.value) invisible(.value)
}, },
format = function() { format = function() {
simpleExprToFunction(fn_body(.origFunc), "reactive") label <- sprintf('reactive(%s)', paste(deparse(body(.origFunc)), collapse='\n'))
strsplit(label, "\n")[[1]]
}, },
.updateValue = function() { .updateValue = function() {
ctx <- Context$new(.domain, .label, type = 'observable', ctx <- Context$new(.domain, .label, type = 'observable',
@@ -951,18 +945,14 @@ Observable <- R6Class(
#' See the [Shiny tutorial](https://shiny.rstudio.com/tutorial/) for #' See the [Shiny tutorial](https://shiny.rstudio.com/tutorial/) for
#' more information about reactive expressions. #' more information about reactive expressions.
#' #'
#' @param x For `is.reactive()`, an object to test. For `reactive()`, an #' @param x For `reactive`, an expression (quoted or unquoted). For
#' expression. When passing in a [`rlang::quo()`]sure with `reactive()`, #' `is.reactive`, an object to test.
#' remember to use [`rlang::inject()`] to distinguish that you are passing in #' @param env The parent environment for the reactive expression. By default,
#' the content of your quosure, not the expression of the quosure. #' this is the calling environment, the same as when defining an ordinary
#' @template param-env #' non-reactive expression.
#' @templateVar x x #' @param quoted Is the expression quoted? By default, this is `FALSE`.
#' @templateVar env env #' This is useful when you want to use an expression that is stored in a
#' @templateVar quoted quoted #' variable; to do so, it must be quoted with `quote()`.
#' @template param-quoted
#' @templateVar x x
#' @templateVar quoted quoted
#' @param label A label for the reactive expression, useful for debugging. #' @param label A label for the reactive expression, useful for debugging.
#' @param domain See [domains]. #' @param domain See [domains].
#' @param ..stacktraceon Advanced use only. For stack manipulation purposes; see #' @param ..stacktraceon Advanced use only. For stack manipulation purposes; see
@@ -971,56 +961,46 @@ Observable <- R6Class(
#' @return a function, wrapped in a S3 class "reactive" #' @return a function, wrapped in a S3 class "reactive"
#' #'
#' @examples #' @examples
#' library(rlang)
#' values <- reactiveValues(A=1) #' values <- reactiveValues(A=1)
#' #'
#' reactiveB <- reactive({ #' reactiveB <- reactive({
#' values$A + 1 #' values$A + 1
#' }) #' })
#' # View the values from the R console with isolate() #'
#' isolate(reactiveB()) #' # Can use quoted expressions
#' # 2 #' reactiveC <- reactive(quote({ values$A + 2 }), quoted = TRUE)
#' #'
#' # To store expressions for later conversion to reactive, use quote() #' # To store expressions for later conversion to reactive, use quote()
#' myquo <- rlang::quo(values$A + 2) #' expr_q <- quote({ values$A + 3 })
#' # Unexpected value! Sending a quosure directly will not work as expected. #' reactiveD <- reactive(expr_q, quoted = TRUE)
#' reactiveC <- reactive(myquo) #'
#' # We'd hope for `3`, but instead we get the quosure that was supplied. #' # View the values from the R console with isolate()
#' isolate(reactiveB())
#' isolate(reactiveC()) #' isolate(reactiveC())
#'
#' # Instead, the quosure should be `rlang::inject()`ed
#' reactiveD <- rlang::inject(reactive(!!myquo))
#' isolate(reactiveD()) #' isolate(reactiveD())
#' # 3
#'
#' # (Legacy) Can use quoted expressions
#' expr <- quote({ values$A + 3 })
#' reactiveE <- reactive(expr, quoted = TRUE)
#' isolate(reactiveE())
#' # 4
#'
#' @export #' @export
reactive <- function( reactive <- function(x, env = parent.frame(), quoted = FALSE,
x,
env = parent.frame(),
quoted = FALSE,
..., ...,
label = NULL, label = NULL,
domain = getDefaultReactiveDomain(), domain = getDefaultReactiveDomain(),
..stacktraceon = TRUE ..stacktraceon = TRUE)
) { {
check_dots_empty() check_dots_empty()
func <- installExprFunction(x, "func", env, quoted, wrappedWithLabel = FALSE) x <- get_quosure(x, env, quoted)
# Attach a label and a reference to the original user source for debugging fun <- as_function(x)
userExpr <- fn_body(func) # as_function returns a function that takes `...`. We need one that takes no
label <- exprToLabel(userExpr, "reactive", label) # args.
formals(fun) <- list()
o <- Observable$new(func, label, domain, ..stacktraceon = ..stacktraceon) # Attach a label and a reference to the original user source for debugging
label <- exprToLabel(get_expr(x), "reactive", label)
o <- Observable$new(fun, label, domain, ..stacktraceon = ..stacktraceon)
structure( structure(
o$getValue, o$getValue,
observable = o, observable = o,
cacheHint = list(userExpr = zap_srcref(userExpr)), cacheHint = list(userExpr = zap_srcref(get_expr(x))),
class = c("reactiveExpr", "reactive", "function") class = c("reactiveExpr", "reactive", "function")
) )
} }
@@ -1213,13 +1193,13 @@ Observer <- R6Class(
# validation = function(e) NULL, # validation = function(e) NULL,
# shiny.output.cancel = function(e) NULL # shiny.output.cancel = function(e) NULL
if (cnd_inherits(e, "shiny.silent.error")) { if (inherits(e, "shiny.silent.error")) {
return() return()
} }
printError(e) printError(e)
if (!is.null(.domain)) { if (!is.null(.domain)) {
.domain$unhandledError(e, close = TRUE) .domain$unhandledError(e)
} }
}, },
finally = .domain$decrementBusyCount finally = .domain$decrementBusyCount
@@ -1345,7 +1325,12 @@ Observer <- R6Class(
#' #'
#' @param x An expression (quoted or unquoted). Any return value will be #' @param x An expression (quoted or unquoted). Any return value will be
#' ignored. #' ignored.
#' @inheritParams reactive #' @param env The parent environment for the reactive expression. By default,
#' this is the calling environment, the same as when defining an ordinary
#' non-reactive expression.
#' @param quoted Is the expression quoted? By default, this is `FALSE`.
#' This is useful when you want to use an expression that is stored in a
#' variable; to do so, it must be quoted with `quote()`.
#' @param label A label for the observer, useful for debugging. #' @param label A label for the observer, useful for debugging.
#' @param suspended If `TRUE`, start the observer in a suspended state. If #' @param suspended If `TRUE`, start the observer in a suspended state. If
#' `FALSE` (the default), start in a non-suspended state. #' `FALSE` (the default), start in a non-suspended state.
@@ -1404,21 +1389,18 @@ Observer <- R6Class(
#' print(values$A + 1) #' print(values$A + 1)
#' }) #' })
#' #'
#' # To store expressions for later conversion to observe, use rlang::quo() #' # Can use quoted expressions
#' myquo <- rlang::quo({ print(values$A + 3) }) #' obsC <- observe(quote({ print(values$A + 2) }), quoted = TRUE)
#' obsC <- rlang::inject(observe(!!myquo))
#' #'
#' # (Legacy) Can use quoted expressions #' # To store expressions for later conversion to observe, use quote()
#' obsD <- observe(quote({ print(values$A + 2) }), quoted = TRUE) #' expr_q <- quote({ print(values$A + 3) })
#' obsD <- observe(expr_q, quoted = TRUE)
#' #'
#' # In a normal Shiny app, the web client will trigger flush events. If you #' # 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() #' # are at the console, you can force a flush with flushReact()
#' shiny:::flushReact() #' shiny:::flushReact()
#' @export #' @export
observe <- function( observe <- function(x, env = parent.frame(), quoted = FALSE,
x,
env = parent.frame(),
quoted = FALSE,
..., ...,
label = NULL, label = NULL,
suspended = FALSE, suspended = FALSE,
@@ -1429,11 +1411,18 @@ observe <- function(
{ {
check_dots_empty() check_dots_empty()
func <- installExprFunction(x, "func", env, quoted) x <- get_quosure(x, env, quoted)
label <- funcToLabel(func, "observe", label) fun <- as_function(x)
# as_function returns a function that takes `...`. We need one that takes no
# args.
formals(fun) <- list()
if (is.null(label)) {
label <- sprintf('observe(%s)', paste(deparse(get_expr(x)), collapse='\n'))
}
o <- Observer$new( o <- Observer$new(
func, fun,
label = label, label = label,
suspended = suspended, suspended = suspended,
priority = priority, priority = priority,
@@ -1926,7 +1915,7 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
#' @export #' @export
reactiveFileReader <- function(intervalMillis, session, filePath, readFunc, ...) { reactiveFileReader <- function(intervalMillis, session, filePath, readFunc, ...) {
filePath <- coerceToFunc(filePath) filePath <- coerceToFunc(filePath)
extraArgs <- list2(...) extraArgs <- list(...)
reactivePoll( reactivePoll(
intervalMillis, session, intervalMillis, session,
@@ -2155,30 +2144,23 @@ maskReactiveContext <- function(expr) {
#' @param valueExpr The expression that produces the return value of the #' @param valueExpr The expression that produces the return value of the
#' `eventReactive`. It will be executed within an [isolate()] #' `eventReactive`. It will be executed within an [isolate()]
#' scope. #' scope.
#' @param event.env The parent environment for the reactive expression. By default, #' @param event.env The parent environment for `eventExpr`. By default,
#' this is the calling environment, the same as when defining an ordinary #' this is the calling environment.
#' non-reactive expression. If `eventExpr` is a quosure and `event.quoted` is `TRUE`, #' @param event.quoted Is the `eventExpr` expression quoted? By default,
#' then `event.env` is ignored. #' this is `FALSE`. This is useful when you want to use an expression
#' @param event.quoted If it is `TRUE`, then the [`quote()`]ed value of `eventExpr` #' that is stored in a variable; to do so, it must be quoted with
#' will be used when `eventExpr` is evaluated. If `eventExpr` is a quosure and you #' `quote()`.
#' would like to use its expression as a value for `eventExpr`, then you must set #' @param handler.env The parent environment for `handlerExpr`. By default,
#' `event.quoted` to `TRUE`. #' this is the calling environment.
#' @param handler.env The parent environment for the reactive expression. By default, #' @param handler.quoted Is the `handlerExpr` expression quoted? By
#' this is the calling environment, the same as when defining an ordinary #' default, this is `FALSE`. This is useful when you want to use an
#' non-reactive expression. If `handlerExpr` is a quosure and `handler.quoted` is `TRUE`, #' expression that is stored in a variable; to do so, it must be quoted with
#' then `handler.env` is ignored. #' `quote()`.
#' @param handler.quoted If it is `TRUE`, then the [`quote()`]ed value of `handlerExpr` #' @param value.env The parent environment for `valueExpr`. By default,
#' will be used when `handlerExpr` is evaluated. If `handlerExpr` is a quosure and you #' this is the calling environment.
#' would like to use its expression as a value for `handlerExpr`, then you must set #' @param value.quoted Is the `valueExpr` expression quoted? By default,
#' `handler.quoted` to `TRUE`. #' this is `FALSE`. This is useful when you want to use an expression
#' @param value.env The parent environment for the reactive expression. By default, #' that is stored in a variable; to do so, it must be quoted with `quote()`.
#' this is the calling environment, the same as when defining an ordinary
#' non-reactive expression. If `valueExpr` is a quosure and `value.quoted` is `TRUE`,
#' then `value.env` is ignored.
#' @param value.quoted If it is `TRUE`, then the [`quote()`]ed value of `valueExpr`
#' will be used when `valueExpr` is evaluated. If `valueExpr` is a quosure and you
#' would like to use its expression as a value for `valueExpr`, then you must set
#' `value.quoted` to `TRUE`.
#' @param label A label for the observer or reactive, useful for debugging. #' @param label A label for the observer or reactive, useful for debugging.
#' @param suspended If `TRUE`, start the observer in a suspended state. If #' @param suspended If `TRUE`, start the observer in a suspended state. If
#' `FALSE` (the default), start in a non-suspended state. #' `FALSE` (the default), start in a non-suspended state.
@@ -2190,8 +2172,8 @@ maskReactiveContext <- function(expr) {
#' @param autoDestroy If `TRUE` (the default), the observer will be #' @param autoDestroy If `TRUE` (the default), the observer will be
#' automatically destroyed when its domain (if any) ends. #' automatically destroyed when its domain (if any) ends.
#' @param ignoreNULL Whether the action should be triggered (or value #' @param ignoreNULL Whether the action should be triggered (or value
#' calculated, in the case of `eventReactive`) when the input event expression #' calculated, in the case of `eventReactive`) when the input is
#' is `NULL`. See Details. #' `NULL`. See Details.
#' @param ignoreInit If `TRUE`, then, when this `observeEvent` is #' @param ignoreInit If `TRUE`, then, when this `observeEvent` is
#' first created/initialized, ignore the `handlerExpr` (the second #' first created/initialized, ignore the `handlerExpr` (the second
#' argument), whether it is otherwise supposed to run or not. The default is #' argument), whether it is otherwise supposed to run or not. The default is
@@ -2292,19 +2274,21 @@ observeEvent <- function(eventExpr, handlerExpr,
{ {
check_dots_empty() check_dots_empty()
eventQ <- exprToQuo(eventExpr, event.env, event.quoted) eventExpr <- get_quosure(eventExpr, event.env, event.quoted)
handlerQ <- exprToQuo(handlerExpr, handler.env, handler.quoted) handlerExpr <- get_quosure(handlerExpr, handler.env, handler.quoted)
label <- quoToLabel(eventQ, "observeEvent", label) if (is.null(label)) {
label <- sprintf('observeEvent(%s)', paste(deparse(get_expr(eventExpr)), collapse='\n'))
}
handler <- inject(observe( handler <- inject(observe(
!!handlerQ, !!handlerExpr,
label = label, label = label,
suspended = suspended, suspended = suspended,
priority = priority, priority = priority,
domain = domain, domain = domain,
autoDestroy = TRUE, autoDestroy = TRUE,
..stacktraceon = TRUE ..stacktraceon = FALSE # TODO: Does this go in the bindEvent?
)) ))
o <- inject(bindEvent( o <- inject(bindEvent(
@@ -2312,7 +2296,7 @@ observeEvent <- function(eventExpr, handlerExpr,
ignoreInit = ignoreInit, ignoreInit = ignoreInit,
once = once, once = once,
label = label, label = label,
!!eventQ, !!eventExpr,
x = handler x = handler
)) ))
@@ -2330,17 +2314,19 @@ eventReactive <- function(eventExpr, valueExpr,
{ {
check_dots_empty() check_dots_empty()
eventQ <- exprToQuo(eventExpr, event.env, event.quoted) eventExpr <- get_quosure(eventExpr, event.env, event.quoted)
valueQ <- exprToQuo(valueExpr, value.env, value.quoted) valueExpr <- get_quosure(valueExpr, value.env, value.quoted)
label <- quoToLabel(eventQ, "eventReactive", label) if (is.null(label)) {
label <- sprintf('eventReactive(%s)', paste(deparse(get_expr(eventExpr)), collapse='\n'))
}
invisible(inject(bindEvent( invisible(inject(bindEvent(
ignoreNULL = ignoreNULL, ignoreNULL = ignoreNULL,
ignoreInit = ignoreInit, ignoreInit = ignoreInit,
label = label, label = label,
!!eventQ, !!eventExpr,
x = reactive(!!valueQ, domain = domain, label = label) x = reactive(!!valueExpr, domain = domain, label = label)
))) )))
} }
@@ -2399,7 +2385,7 @@ isNullEvent <- function(value) {
#' reactive recently (within the time window) invalidated. New `r` #' reactive recently (within the time window) invalidated. New `r`
#' invalidations do not reset the time window. This means that if invalidations #' invalidations do not reset the time window. This means that if invalidations
#' continually come from `r` within the time window, the throttled reactive #' continually come from `r` within the time window, the throttled reactive
#' will invalidate regularly, at a rate equal to or slower than the time #' will invalidate regularly, at a rate equal to or slower than than the time
#' window. #' window.
#' #'
#' `ooo-oo-oo---- => o--o--o--o---` #' `ooo-oo-oo---- => o--o--o--o---`
@@ -2482,11 +2468,11 @@ debounce <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
# Ensure r() is called only after setting firstRun to FALSE since r() # Ensure r() is called only after setting firstRun to FALSE since r()
# may throw an error # may throw an error
try(r(), silent = TRUE) r()
return() return()
} }
# This ensures r() is still tracked after firstRun # This ensures r() is still tracked after firstRun
try(r(), silent = TRUE) r()
# The value (or possibly millis) changed. Start or reset the timer. # The value (or possibly millis) changed. Start or reset the timer.
v$when <- getDomainTimeMs(domain) + millis() v$when <- getDomainTimeMs(domain) + millis()
@@ -2519,7 +2505,7 @@ debounce <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
# commenting it out and studying the unit test failure that results. # commenting it out and studying the unit test failure that results.
primer <- observe({ primer <- observe({
primer$destroy() primer$destroy()
try(er(), silent = TRUE) er()
}, label = "debounce primer", domain = domain, priority = priority) }, label = "debounce primer", domain = domain, priority = priority)
er er
@@ -2561,7 +2547,7 @@ throttle <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
} }
# Responsible for tracking when f() changes. # Responsible for tracking when f() changes.
observeEvent(try(r(), silent = TRUE), { observeEvent(r(), {
if (v$pending) { if (v$pending) {
# In a blackout period and someone already scheduled; do nothing # In a blackout period and someone already scheduled; do nothing
} else if (blackoutMillisLeft() > 0) { } else if (blackoutMillisLeft() > 0) {

View File

@@ -1,6 +1,6 @@
#### ####
# Generated by `./tools/documentation/updateReexports.R`: do not edit by hand # Generated by `./tools/updateReexports.R`: do not edit by hand
# Please call `source('tools/documentation/updateReexports.R')` from the root folder to update` # Please call `source('tools/updateReexports.R') from the root folder to update`
#### ####
@@ -90,20 +90,17 @@ htmltools::em
#' @export #' @export
htmltools::hr htmltools::hr
# htmltools tag.Rd -------------------------------------------------------------
#' @importFrom htmltools tag #' @importFrom htmltools tag
#' @export #' @export
htmltools::tag htmltools::tag
# htmltools tagList.Rd ---------------------------------------------------------
#' @importFrom htmltools tagList #' @importFrom htmltools tagList
#' @export #' @export
htmltools::tagList htmltools::tagList
# htmltools tagAppendAttributes.Rd ---------------------------------------------
#' @importFrom htmltools tagAppendAttributes #' @importFrom htmltools tagAppendAttributes
#' @export #' @export
htmltools::tagAppendAttributes htmltools::tagAppendAttributes
@@ -116,9 +113,6 @@ htmltools::tagHasAttribute
#' @export #' @export
htmltools::tagGetAttribute htmltools::tagGetAttribute
# htmltools tagAppendChild.Rd --------------------------------------------------
#' @importFrom htmltools tagAppendChild #' @importFrom htmltools tagAppendChild
#' @export #' @export
htmltools::tagAppendChild htmltools::tagAppendChild

View File

@@ -181,7 +181,7 @@
#' # At the top of app.R, this set the application-scoped cache to be a disk #' # At the top of app.R, this set the application-scoped cache to be a disk
#' # cache that can be shared among multiple concurrent R processes, and is #' # cache that can be shared among multiple concurrent R processes, and is
#' # deleted when the system reboots. #' # deleted when the system reboots.
#' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))) #' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))
#' #'
#' # At the top of app.R, this set the application-scoped cache to be a disk #' # At the top of app.R, this set the application-scoped cache to be a disk
#' # cache that can be shared among multiple concurrent R processes, and #' # cache that can be shared among multiple concurrent R processes, and

View File

@@ -34,19 +34,19 @@
#' When rendering an inline plot, you must provide numeric values (in pixels) #' When rendering an inline plot, you must provide numeric values (in pixels)
#' to both \code{width} and \code{height}. #' to both \code{width} and \code{height}.
#' @param res Resolution of resulting plot, in pixels per inch. This value is #' @param res Resolution of resulting plot, in pixels per inch. This value is
#' passed to [plotPNG()]. Note that this affects the resolution of PNG #' passed to [grDevices::png()]. Note that this affects the resolution of PNG
#' rendering in R; it won't change the actual ppi of the browser. #' rendering in R; it won't change the actual ppi of the browser.
#' @param alt Alternate text for the HTML `<img>` tag if it cannot be displayed #' @param alt Alternate text for the HTML `<img>` tag
#' or viewed (i.e., the user uses a screen reader). In addition to a character #' if it cannot be displayed or viewed (i.e., the user uses a screen reader).
#' string, the value may be a reactive expression (or a function referencing #' In addition to a character string, the value may be a reactive expression
#' reactive values) that returns a character string. If the value is `NA` (the #' (or a function referencing reactive values) that returns a character string.
#' default), then `ggplot2::get_alt_text()` is used to extract alt text from #' NULL or "" is not recommended because those should be limited to decorative images
#' ggplot objects; for other plots, `NA` results in alt text of "Plot object". #' (the default is "Plot object").
#' `NULL` or `""` is not recommended because those should be limited to #' @param ... Arguments to be passed through to [grDevices::png()].
#' decorative images.
#' @param ... Arguments to be passed through to [plotPNG()].
#' These can be used to set the width, height, background color, etc. #' These can be used to set the width, height, background color, etc.
#' @inheritParams renderUI #' @param env The environment in which to evaluate `expr`.
#' @param quoted Is `expr` a quoted expression (with `quote()`)? This
#' is useful if you want to save an expression in a variable.
#' @param execOnResize If `FALSE` (the default), then when a plot is #' @param execOnResize If `FALSE` (the default), then when a plot is
#' resized, Shiny will *replay* the plot drawing commands with #' resized, Shiny will *replay* the plot drawing commands with
#' [grDevices::replayPlot()] instead of re-executing `expr`. #' [grDevices::replayPlot()] instead of re-executing `expr`.
@@ -58,18 +58,15 @@
#' interactive R Markdown document. #' interactive R Markdown document.
#' @export #' @export
renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ..., renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
alt = NA, alt = "Plot object",
env = parent.frame(), quoted = FALSE, env = parent.frame(), quoted = FALSE,
execOnResize = FALSE, outputArgs = list() execOnResize = FALSE, outputArgs = list()
) { ) {
func <- installExprFunction( expr <- get_quosure(expr, env, quoted)
expr, "func", env, quoted, # This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
label = "renderPlot", # is called
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc func <- quoToFunction(expr, "renderPlot", ..stacktraceon = TRUE)
# is called
..stacktraceon = TRUE
)
args <- list(...) args <- list(...)
@@ -187,15 +184,15 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
outputFunc, outputFunc,
renderFunc, renderFunc,
outputArgs, outputArgs,
cacheHint = list(userExpr = installedFuncExpr(func), res = res) cacheHint = list(userExpr = get_expr(expr), res = res)
) )
class(markedFunc) <- c("shiny.renderPlot", class(markedFunc)) class(markedFunc) <- c("shiny.renderPlot", class(markedFunc))
markedFunc markedFunc
} }
resizeSavedPlot <- function(name, session, result, width, height, alt, pixelratio, res, ...) { resizeSavedPlot <- function(name, session, result, width, height, alt, pixelratio, res, ...) {
if (isTRUE(result$img$width == width && result$img$height == height && if (result$img$width == width && result$img$height == height &&
result$pixelratio == pixelratio && result$res == res)) { result$pixelratio == pixelratio && result$res == res) {
return(result) return(result)
} }
@@ -215,7 +212,7 @@ resizeSavedPlot <- function(name, session, result, width, height, alt, pixelrati
src = session$fileUrl(name, outfile, contentType = "image/png"), src = session$fileUrl(name, outfile, contentType = "image/png"),
width = width, width = width,
height = height, height = height,
alt = result$alt, alt = alt,
coordmap = coordmap, coordmap = coordmap,
error = attr(coordmap, "error", exact = TRUE) error = attr(coordmap, "error", exact = TRUE)
) )
@@ -266,8 +263,6 @@ drawPlot <- function(name, session, func, width, height, alt, pixelratio, res, .
# addition to ggplot, and there's a print method for that class, that we # 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 # won't override that method. https://github.com/rstudio/shiny/issues/841
print.ggplot <- custom_print.ggplot print.ggplot <- custom_print.ggplot
# For compatibility with ggplot2 >v4.0.0
`print.ggplot2::ggplot` <- custom_print.ggplot
# Use capture.output to squelch printing to the actual console; we # Use capture.output to squelch printing to the actual console; we
# are only interested in plot output # are only interested in plot output
@@ -293,7 +288,6 @@ drawPlot <- function(name, session, func, width, height, alt, pixelratio, res, .
recordedPlot = grDevices::recordPlot(), recordedPlot = grDevices::recordPlot(),
coordmap = getCoordmap(value, width*pixelratio, height*pixelratio, res*pixelratio), coordmap = getCoordmap(value, width*pixelratio, height*pixelratio, res*pixelratio),
pixelratio = pixelratio, pixelratio = pixelratio,
alt = if (anyNA(alt)) getAltText(value) else alt,
res = res res = res
) )
} }
@@ -308,10 +302,10 @@ drawPlot <- function(name, session, func, width, height, alt, pixelratio, res, .
), ),
function(result) { function(result) {
result$img <- dropNulls(list( result$img <- dropNulls(list(
src = session$fileUrl(name, outfile, contentType = 'image/png'), src = session$fileUrl(name, outfile, contentType='image/png'),
width = width, width = width,
height = height, height = height,
alt = result$alt, alt = alt,
coordmap = result$coordmap, coordmap = result$coordmap,
# Get coordmap error message if present # Get coordmap error message if present
error = attr(result$coordmap, "error", exact = TRUE) error = attr(result$coordmap, "error", exact = TRUE)
@@ -345,24 +339,6 @@ custom_print.ggplot <- function(x) {
), class = "ggplot_build_gtable") ), class = "ggplot_build_gtable")
} }
# Infer alt text description from renderPlot() value
# (currently just ggplot2 is supported)
getAltText <- function(x, default = "Plot object") {
# Since, inside renderPlot(), custom_print.ggplot()
# overrides print.ggplot, this class indicates a ggplot()
if (!inherits(x, "ggplot_build_gtable")) {
return(default)
}
# ggplot2::get_alt_text() was added in v3.3.4
# https://github.com/tidyverse/ggplot2/pull/4482
get_alt <- getNamespace("ggplot2")$get_alt_text
if (!is.function(get_alt)) {
return(default)
}
alt <- paste(get_alt(x$build), collapse = " ")
if (nzchar(alt)) alt else default
}
# The coordmap extraction functions below return something like the examples # The coordmap extraction functions below return something like the examples
# below. For base graphics: # below. For base graphics:
# plot(mtcars$wt, mtcars$mpg) # plot(mtcars$wt, mtcars$mpg)
@@ -614,7 +590,7 @@ getGgplotCoordmap <- function(p, width, height, res) {
find_panel_info <- function(b) { find_panel_info <- function(b) {
# Structure of ggplot objects changed after 2.1.0. After 2.2.1, there was a # Structure of ggplot objects changed after 2.1.0. After 2.2.1, there was a
# an API for extracting the necessary information. # an API for extracting the necessary information.
ggplot_ver <- get_package_version("ggplot2") ggplot_ver <- utils::packageVersion("ggplot2")
if (ggplot_ver > "2.2.1") { if (ggplot_ver > "2.2.1") {
find_panel_info_api(b) find_panel_info_api(b)

View File

@@ -42,7 +42,9 @@
#' (i.e. they either evaluate to `NA` or `NaN`). #' (i.e. they either evaluate to `NA` or `NaN`).
#' @param ... Arguments to be passed through to [xtable::xtable()] #' @param ... Arguments to be passed through to [xtable::xtable()]
#' and [xtable::print.xtable()]. #' and [xtable::print.xtable()].
#' @inheritParams renderUI #' @param env The environment in which to evaluate `expr`.
#' @param quoted Is `expr` a quoted expression (with `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 #' @param outputArgs A list of arguments to be passed through to the
#' implicit call to [tableOutput()] when `renderTable` is #' implicit call to [tableOutput()] when `renderTable` is
#' used in an interactive R Markdown document. #' used in an interactive R Markdown document.
@@ -72,7 +74,8 @@ renderTable <- function(expr, striped = FALSE, hover = FALSE,
env = parent.frame(), quoted = FALSE, env = parent.frame(), quoted = FALSE,
outputArgs=list()) outputArgs=list())
{ {
func <- installExprFunction(expr, "func", env, quoted, label = "renderTable") expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderTable")
if (!is.function(spacing)) spacing <- match.arg(spacing) if (!is.function(spacing)) spacing <- match.arg(spacing)

View File

@@ -23,10 +23,10 @@
#' @examples #' @examples
#' ## Only run this example in interactive R sessions #' ## Only run this example in interactive R sessions
#' if (interactive()) { #' if (interactive()) {
#' runUrl('https://github.com/rstudio/shiny_example/archive/main.tar.gz') #' runUrl('https://github.com/rstudio/shiny_example/archive/master.tar.gz')
#' #'
#' # Can run an app from a subdirectory in the archive #' # Can run an app from a subdirectory in the archive
#' runUrl("https://github.com/rstudio/shiny_example/archive/main.zip", #' runUrl("https://github.com/rstudio/shiny_example/archive/master.zip",
#' subdir = "inst/shinyapp/") #' subdir = "inst/shinyapp/")
#' } #' }
runUrl <- function(url, filetype = NULL, subdir = NULL, destdir = NULL, ...) { runUrl <- function(url, filetype = NULL, subdir = NULL, destdir = NULL, ...) {
@@ -121,8 +121,7 @@ runGist <- function(gist, destdir = NULL, ...) {
#' @param username GitHub username. If `repo` is of the form #' @param username GitHub username. If `repo` is of the form
#' `"username/repo"`, `username` will be taken from `repo`. #' `"username/repo"`, `username` will be taken from `repo`.
#' @param ref Desired git reference. Could be a commit, tag, or branch name. #' @param ref Desired git reference. Could be a commit, tag, or branch name.
#' Defaults to `"HEAD"`, which means the default branch on GitHub, typically #' Defaults to `"master"`.
#' `"main"` or `"master"`.
#' @export #' @export
#' @examples #' @examples
#' ## Only run this example in interactive R sessions #' ## Only run this example in interactive R sessions
@@ -134,7 +133,7 @@ runGist <- function(gist, destdir = NULL, ...) {
#' runGitHub("shiny_example", "rstudio", subdir = "inst/shinyapp/") #' runGitHub("shiny_example", "rstudio", subdir = "inst/shinyapp/")
#' } #' }
runGitHub <- function(repo, username = getOption("github.user"), runGitHub <- function(repo, username = getOption("github.user"),
ref = "HEAD", subdir = NULL, destdir = NULL, ...) { ref = "master", subdir = NULL, destdir = NULL, ...) {
if (grepl('/', repo)) { if (grepl('/', repo)) {
res <- strsplit(repo, '/')[[1]] res <- strsplit(repo, '/')[[1]]

View File

@@ -22,13 +22,10 @@
#' @param port The TCP port that the application should listen on. If the #' @param port The TCP port that the application should listen on. If the
#' `port` is not specified, and the `shiny.port` option is set (with #' `port` is not specified, and the `shiny.port` option is set (with
#' `options(shiny.port = XX)`), then that port will be used. Otherwise, #' `options(shiny.port = XX)`), then that port will be used. Otherwise,
#' use a random port between 3000:8000, excluding ports that are blocked #' use a random port.
#' by Google Chrome for being considered unsafe: 3659, 4045, 5060,
#' 5061, 6000, 6566, 6665:6669 and 6697. Up to twenty random
#' ports will be tried.
#' @param launch.browser If true, the system's default web browser will be #' @param launch.browser If true, the system's default web browser will be
#' launched automatically after the app is started. Defaults to true in #' launched automatically after the app is started. Defaults to true in
#' interactive sessions only. The value of this parameter can also be a #' interactive sessions only. This value of this parameter can also be a
#' function to call with the application's URL. #' function to call with the application's URL.
#' @param host The IPv4 address that the application should listen on. Defaults #' @param host The IPv4 address that the application should listen on. Defaults
#' to the `shiny.host` option, if set, or `"127.0.0.1"` if not. See #' to the `shiny.host` option, if set, or `"127.0.0.1"` if not. See
@@ -304,8 +301,7 @@ runApp <- function(appDir=getwd(),
# Reject ports in this range that are considered unsafe by Chrome # Reject ports in this range that are considered unsafe by Chrome
# http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome # http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
# https://github.com/rstudio/shiny/issues/1784 # https://github.com/rstudio/shiny/issues/1784
# https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/net/base/port_util.cc if (!port %in% c(3659, 4045, 6000, 6665:6669, 6697)) {
if (!port %in% c(3659, 4045, 5060, 5061, 6000, 6566, 6665:6669, 6697)) {
break break
} }
} }
@@ -445,20 +441,8 @@ stopApp <- function(returnValue = invisible()) {
#' @param host The IPv4 address that the application should listen on. Defaults #' @param host The IPv4 address that the application should listen on. Defaults
#' to the `shiny.host` option, if set, or `"127.0.0.1"` if not. #' to the `shiny.host` option, if set, or `"127.0.0.1"` if not.
#' @param display.mode The mode in which to display the example. Defaults to #' @param display.mode The mode in which to display the example. Defaults to
#' `"auto"`, which uses the value of `DisplayMode` in the example's #' `showcase`, but may be set to `normal` to see the example without
#' `DESCRIPTION` file. Set to `"showcase"` to show the app code and
#' description with the running app, or `"normal"` to see the example without
#' code or commentary. #' code or commentary.
#' @param package The package in which to find the example (defaults to
#' `"shiny"`).
#'
#' To provide examples in your package, store examples in the
#' `inst/examples-shiny` directory of your package. Each example should be
#' in its own subdirectory and should be runnable when [runApp()] is called
#' on the subdirectory. Example apps can include a `DESCRIPTION` file and a
#' `README.md` file to provide metadata and commentary about the example. See
#' the article on [Display Modes](https://shiny.posit.co/r/articles/build/display-modes/)
#' on the Shiny website for more information.
#' @inheritParams runApp #' @inheritParams runApp
#' #'
#' @examples #' @examples
@@ -474,46 +458,32 @@ stopApp <- function(returnValue = invisible()) {
#' system.file("examples", package="shiny") #' system.file("examples", package="shiny")
#' } #' }
#' @export #' @export
runExample <- function( runExample <- function(example=NA,
example = NA, port=getOption("shiny.port"),
port = getOption("shiny.port"), launch.browser = getOption('shiny.launch.browser', interactive()),
launch.browser = getOption("shiny.launch.browser", interactive()), host=getOption('shiny.host', '127.0.0.1'),
host = getOption("shiny.host", "127.0.0.1"), display.mode=c("auto", "normal", "showcase")) {
display.mode = c("auto", "normal", "showcase"), examplesDir <- system.file('examples', package='shiny')
package = "shiny"
) {
if (!identical(package, "shiny") && !is_installed(package)) {
rlang::check_installed(package)
}
use_legacy_shiny_examples <-
identical(package, "shiny") &&
isTRUE(getOption('shiny.legacy.examples', FALSE))
examplesDir <- system_file(
if (use_legacy_shiny_examples) "examples" else "examples-shiny",
package = package
)
dir <- resolve(examplesDir, example) dir <- resolve(examplesDir, example)
if (is.null(dir)) { if (is.null(dir)) {
valid_examples <- sprintf(
'Valid examples in {%s}: "%s"',
package,
paste(list.files(examplesDir), collapse = '", "')
)
if (is.na(example)) { if (is.na(example)) {
message(valid_examples) errFun <- message
return(invisible()) errMsg <- ''
}
else {
errFun <- stop
errMsg <- paste('Example', example, 'does not exist. ')
} }
stop("Example '", example, "' does not exist. ", valid_examples) errFun(errMsg,
'Valid examples are "',
paste(list.files(examplesDir), collapse='", "'),
'"')
}
else {
runApp(dir, port = port, host = host, launch.browser = launch.browser,
display.mode = display.mode)
} }
runApp(dir, port = port, host = host, launch.browser = launch.browser,
display.mode = display.mode)
} }
#' Run a gadget #' Run a gadget

View File

@@ -5,6 +5,7 @@
#' value. The returned value will be used for the test snapshot. #' value. The returned value will be used for the test snapshot.
#' @param session A Shiny session object. #' @param session A Shiny session object.
#' #'
#' @keywords internal
#' @export #' @export
setSerializer <- function(inputId, fun, session = getDefaultReactiveDomain()) { setSerializer <- function(inputId, fun, session = getDefaultReactiveDomain()) {
if (is.null(session)) { if (is.null(session)) {

View File

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

View File

@@ -1,12 +1,7 @@
#' @include server-input-handlers.R #' @include server-input-handlers.R
appsByToken <- NULL appsByToken <- Map$new()
appsNeedingFlush <- NULL appsNeedingFlush <- Map$new()
on_load({
appsByToken <- Map$new()
appsNeedingFlush <- Map$new()
})
# Provide a character representation of the WS that can be used # Provide a character representation of the WS that can be used
# as a key in a Map. # as a key in a Map.
@@ -34,7 +29,7 @@ registerClient <- function(client) {
#' Define Server Functionality #' Define Server Functionality
#' #'
#' @description `r lifecycle::badge("superseded")` #' @description \lifecycle{superseded}
#' #'
#' @description Defines the server-side logic of the Shiny application. This generally #' @description Defines the server-side logic of the Shiny application. This generally
#' involves creating functions that map user inputs to various kinds of output. #' involves creating functions that map user inputs to various kinds of output.
@@ -54,7 +49,7 @@ registerClient <- function(client) {
#' optional `session` parameter, which is used when greater control is #' optional `session` parameter, which is used when greater control is
#' needed. #' needed.
#' #'
#' See the [tutorial](https://shiny.rstudio.com/tutorial/) for more #' See the [tutorial](https://rstudio.github.io/shiny/tutorial/) for more
#' on how to write a server function. #' on how to write a server function.
#' #'
#' @param func The server function for this application. See the details section #' @param func The server function for this application. See the details section
@@ -127,16 +122,13 @@ decodeMessage <- function(data) {
return(mainMessage) return(mainMessage)
} }
autoReloadCallbacks <- NULL autoReloadCallbacks <- Callbacks$new()
on_load({
autoReloadCallbacks <- Callbacks$new()
})
createAppHandlers <- function(httpHandlers, serverFuncSource) { createAppHandlers <- function(httpHandlers, serverFuncSource) {
appvars <- new.env() appvars <- new.env()
appvars$server <- NULL appvars$server <- NULL
sys.www.root <- system_file('www', package='shiny') sys.www.root <- system.file('www', package='shiny')
# This value, if non-NULL, must be present on all HTTP and WebSocket # This value, if non-NULL, must be present on all HTTP and WebSocket
# requests as the Shiny-Shared-Secret header or else access will be # requests as the Shiny-Shared-Secret header or else access will be
@@ -339,7 +331,7 @@ argsForServerFunc <- function(serverFunc, session) {
getEffectiveBody <- function(func) { getEffectiveBody <- function(func) {
if (is.null(func)) if (is.null(func))
NULL NULL
else if (isS4(func) && inherits(func, "functionWithTrace")) else if (isS4(func) && class(func) == "functionWithTrace")
body(func@original) body(func@original)
else else
body(func) body(func)
@@ -393,7 +385,7 @@ startApp <- function(appObj, port, host, quiet) {
list( list(
# Always handle /session URLs dynamically, even if / is a static path. # Always handle /session URLs dynamically, even if / is a static path.
"session" = excludeStaticPath(), "session" = excludeStaticPath(),
"shared" = system_file(package = "shiny", "www", "shared") "shared" = system.file(package = "shiny", "www", "shared")
), ),
.globals$resourcePaths .globals$resourcePaths
) )

View File

@@ -65,20 +65,16 @@ getShinyOption <- function(name, default = NULL) {
#' changes are detected, all connected Shiny sessions are reloaded. This #' changes are detected, all connected Shiny sessions are reloaded. This
#' allows for fast feedback loops when tweaking Shiny UI. #' allows for fast feedback loops when tweaking Shiny UI.
#' #'
#' Monitoring for changes is no longer expensive, thanks to the \pkg{watcher} #' Since monitoring for changes is expensive (we simply poll for last
#' package, but this feature is still intended only for development. #' modified times), this feature is intended only for development.
#' #'
#' You can customize the file patterns Shiny will monitor by setting the #' You can customize the file patterns Shiny will monitor by setting the
#' shiny.autoreload.pattern option. For example, to monitor only `ui.R`: #' shiny.autoreload.pattern option. For example, to monitor only ui.R:
#' `options(shiny.autoreload.pattern = glob2rx("ui.R"))`. #' `options(shiny.autoreload.pattern = glob2rx("ui.R"))`
#' #'
#' As mentioned above, Shiny no longer polls watched files for changes. #' The default polling interval is 500 milliseconds. You can change this
#' Instead, using \pkg{watcher}, Shiny is notified of file changes as they #' by setting e.g. `options(shiny.autoreload.interval = 2000)` (every
#' occur. These changes are batched together within a customizable latency #' two seconds).}
#' period. You can adjust this period by setting
#' `options(shiny.autoreload.interval = 2000)` (in milliseconds). This value
#' converted to seconds and passed to the `latency` argument of
#' [watcher::watcher()]. The default latency is 250ms.}
#' \item{shiny.deprecation.messages (defaults to `TRUE`)}{This controls whether messages for #' \item{shiny.deprecation.messages (defaults to `TRUE`)}{This controls whether messages for
#' deprecated functions in Shiny will be printed. See #' deprecated functions in Shiny will be printed. See
#' [shinyDeprecated()] for more information.} #' [shinyDeprecated()] for more information.}
@@ -94,15 +90,10 @@ getShinyOption <- function(name, default = NULL) {
#' \item{shiny.jquery.version (defaults to `3`)}{The major version of jQuery to use. #' \item{shiny.jquery.version (defaults to `3`)}{The major version of jQuery to use.
#' Currently only values of `3` or `1` are supported. If `1`, then jQuery 1.12.4 is used. If `3`, #' Currently only values of `3` or `1` are supported. If `1`, then jQuery 1.12.4 is used. If `3`,
#' then jQuery `r version_jquery` is used.} #' then jQuery `r version_jquery` is used.}
#' \item{shiny.json.digits (defaults to `I(16)`)}{Max number of digits to use when converting #' \item{shiny.json.digits (defaults to `16`)}{The number of digits to use when converting
#' numbers to JSON format to send to the client web browser. Use [I()] to specify significant digits. #' numbers to JSON format to send to the client web browser.}
#' Use `NA` for max precision.}
#' \item{shiny.launch.browser (defaults to `interactive()`)}{A boolean which controls the default behavior #' \item{shiny.launch.browser (defaults to `interactive()`)}{A boolean which controls the default behavior
#' when an app is run. See [runApp()] for more information.} #' when an app is run. See [runApp()] for more information.}
#' \item{shiny.mathjax.url (defaults to `"https://mathjax.rstudio.com/latest/MathJax.js"`)}{
#' The URL that should be used to load MathJax, via [withMathJax()].}
#' \item{shiny.mathjax.config (defaults to `"config=TeX-AMS-MML_HTMLorMML"`)}{The querystring
#' used to load MathJax, via [withMathJax()].}
#' \item{shiny.maxRequestSize (defaults to 5MB)}{This is a number which specifies the maximum #' \item{shiny.maxRequestSize (defaults to 5MB)}{This is a number which specifies the maximum
#' web request size, which serves as a size limit for file uploads.} #' web request size, which serves as a size limit for file uploads.}
#' \item{shiny.minified (defaults to `TRUE`)}{By default #' \item{shiny.minified (defaults to `TRUE`)}{By default
@@ -117,7 +108,7 @@ getShinyOption <- function(name, default = NULL) {
#' production.} #' production.}
#' \item{shiny.sanitize.errors (defaults to `FALSE`)}{If `TRUE`, then normal errors (i.e. #' \item{shiny.sanitize.errors (defaults to `FALSE`)}{If `TRUE`, then normal errors (i.e.
#' errors not wrapped in `safeError`) won't show up in the app; a simple #' errors not wrapped in `safeError`) won't show up in the app; a simple
#' generic error message is printed instead (the error and stack trace printed #' generic error message is printed instead (the error and strack trace printed
#' to the console remain unchanged). If you want to sanitize errors in general, but you DO want a #' to the console remain unchanged). If you want to sanitize errors in general, but you DO want a
#' particular error `e` to get displayed to the user, then set this option #' particular error `e` to get displayed to the user, then set this option
#' to `TRUE` and use `stop(safeError(e))` for errors you want the #' to `TRUE` and use `stop(safeError(e))` for errors you want the
@@ -134,9 +125,6 @@ getShinyOption <- function(name, default = NULL) {
#' console.} #' console.}
#' \item{shiny.testmode (defaults to `FALSE`)}{If `TRUE`, then various features for testing Shiny #' \item{shiny.testmode (defaults to `FALSE`)}{If `TRUE`, then various features for testing Shiny
#' applications are enabled.} #' applications are enabled.}
#' \item{shiny.snapshotsortc (defaults to `FALSE`)}{If `TRUE`, test snapshot keys
#' for \pkg{shinytest} will be sorted consistently using the C locale. Snapshots
#' retrieved by \pkg{shinytest2} will always sort using the C locale.}
#' \item{shiny.trace (defaults to `FALSE`)}{Print messages sent between the R server and the web #' \item{shiny.trace (defaults to `FALSE`)}{Print messages sent between the R server and the web
#' browser client to the R console. This is useful for debugging. Possible #' browser client to the R console. This is useful for debugging. Possible
#' values are `"send"` (only print messages sent to the client), #' values are `"send"` (only print messages sent to the client),
@@ -145,21 +133,15 @@ getShinyOption <- function(name, default = NULL) {
#' messages).} #' messages).}
#' \item{shiny.autoload.r (defaults to `TRUE`)}{If `TRUE`, then the R/ #' \item{shiny.autoload.r (defaults to `TRUE`)}{If `TRUE`, then the R/
#' of a shiny app will automatically be sourced.} #' of a shiny app will automatically be sourced.}
#' \item{shiny.useragg (defaults to `TRUE`)}{Set to `FALSE` to prevent PNG rendering via the #' \item{shiny.usecairo (defaults to `TRUE`)}{This is used to disable graphical rendering by the
#' ragg package. See [plotPNG()] for more information.} #' Cairo package, if it is installed. See [plotPNG()] for more
#' \item{shiny.usecairo (defaults to `TRUE`)}{Set to `FALSE` to prevent PNG rendering via the #' information.}
#' Cairo package. See [plotPNG()] for more information.}
#' \item{shiny.devmode (defaults to `NULL`)}{Option to enable Shiny Developer Mode. When set, #' \item{shiny.devmode (defaults to `NULL`)}{Option to enable Shiny Developer Mode. When set,
#' different default `getOption(key)` values will be returned. See [devmode()] for more details.} #' different default `getOption(key)` values will be returned. See [devmode()] for more details.}
### Not documenting as 'shiny.devmode.verbose' is for niche use only ### Not documenting as 'shiny.devmode.verbose' is for niche use only
# ' \item{shiny.devmode.verbose (defaults to `TRUE`)}{If `TRUE`, will display messages printed # ' \item{shiny.devmode.verbose (defaults to `TRUE`)}{If `TRUE`, will display messages printed
# ' about which options are being set. See [devmode()] for more details. } # ' about which options are being set. See [devmode()] for more details. }
### (end not documenting 'shiny.devmode.verbose') ### (end not documenting 'shiny.devmode.verbose')
### start shiny.client_devmode is primarily for niche, internal shinylive usage
# ' \item{shiny.client_devmode (defaults to `FALSE`)}{If `TRUE`, enables client-
# ' side devmode features. Currently the primary feature is the client-side
# ' error console.}
### end shiny.client_devmode
#' } #' }
#' #'
#' #'
@@ -196,7 +178,7 @@ getShinyOption <- function(name, default = NULL) {
#' @aliases shiny-options #' @aliases shiny-options
#' @export #' @export
shinyOptions <- function(...) { shinyOptions <- function(...) {
newOpts <- list2(...) newOpts <- list(...)
if (length(newOpts) > 0) { if (length(newOpts) > 0) {
# If we're within a session, modify at the session level. # If we're within a session, modify at the session level.

View File

@@ -1,7 +1,8 @@
# See also R/reexports.R # See also R/reexports.R
## usethis namespace: start ## usethis namespace: start
#' @importFrom lifecycle deprecated is_present ## usethis namespace: end
#' @importFrom lifecycle deprecated
#' @importFrom grDevices dev.set dev.cur #' @importFrom grDevices dev.set dev.cur
#' @importFrom fastmap fastmap #' @importFrom fastmap fastmap
#' @importFrom promises %...!% #' @importFrom promises %...!%
@@ -10,20 +11,18 @@
#' promise promise_resolve promise_reject is.promising #' promise promise_resolve promise_reject is.promising
#' as.promise #' as.promise
#' @importFrom rlang #' @importFrom rlang
#' quo enquo enquo0 as_function get_expr get_env new_function enquos #' quo enquo as_function get_expr get_env new_function enquos
#' eval_tidy expr pairlist2 new_quosure enexpr as_quosure is_quosure inject #' eval_tidy expr pairlist2 new_quosure enexpr as_quosure is_quosure inject
#' quo_set_env quo_set_expr quo_get_expr
#' enquos0 zap_srcref %||% is_na #' enquos0 zap_srcref %||% is_na
#' is_false list2 #' is_false
#' missing_arg is_missing maybe_missing #' missing_arg is_missing maybe_missing
#' quo_is_missing fn_fmls<- fn_body fn_body<- #' @importFrom ellipsis
#' check_dots_empty check_dots_unnamed #' check_dots_empty check_dots_unnamed
#' @import htmltools #' @import htmltools
#' @import httpuv #' @import httpuv
#' @import xtable #' @import xtable
#' @import R6 #' @import R6
#' @import mime #' @import mime
## usethis namespace: end
NULL NULL
# It's necessary to Depend on methods so Rscript doesn't fail. It's necessary # It's necessary to Depend on methods so Rscript doesn't fail. It's necessary
@@ -33,11 +32,3 @@ NULL
# since we call require(shiny) as part of loading the app. # since we call require(shiny) as part of loading the app.
#' @import methods #' @import methods
NULL NULL
# For usethis::use_release_issue()
release_bullets <- function() {
c(
"Update static imports: `staticimports::import()`"
)
}

254
R/shiny.R
View File

@@ -16,7 +16,8 @@ NULL
#' #'
#' @name shiny-package #' @name shiny-package
#' @aliases shiny #' @aliases shiny
"_PACKAGE" #' @docType package
NULL
createUniqueId <- function(bytes, prefix = "", suffix = "") { createUniqueId <- function(bytes, prefix = "", suffix = "") {
withPrivateSeed({ withPrivateSeed({
@@ -32,12 +33,8 @@ createUniqueId <- function(bytes, prefix = "", suffix = "") {
} }
toJSON <- function(x, ..., dataframe = "columns", null = "null", na = "null", toJSON <- function(x, ..., dataframe = "columns", null = "null", na = "null",
auto_unbox = TRUE, auto_unbox = TRUE, digits = getOption("shiny.json.digits", 16),
# Shiny has had a legacy value of 16 significant digits use_signif = TRUE, force = TRUE, POSIXt = "ISO8601", UTC = TRUE,
# We can use `I(16)` mixed with the default behavior in jsonlite's `use_signif=`
# https://github.com/jeroen/jsonlite/commit/728efa9
digits = getOption("shiny.json.digits", I(16)), use_signif = is(digits, "AsIs"),
force = TRUE, POSIXt = "ISO8601", UTC = TRUE,
rownames = FALSE, keep_vec_names = TRUE, strict_atomic = TRUE) { rownames = FALSE, keep_vec_names = TRUE, strict_atomic = TRUE) {
if (strict_atomic) { if (strict_atomic) {
@@ -188,11 +185,9 @@ workerId <- local({
#' session is actually connected. #' session is actually connected.
#' } #' }
#' \item{request}{ #' \item{request}{
#' An environment that implements the [Rook #' An environment that implements the Rook specification for HTTP requests.
#' specification](https://github.com/jeffreyhorner/Rook#the-environment) for #' This is the request that was used to initiate the websocket connection
#' HTTP requests. This is the request that was used to initiate the websocket #' (as opposed to the request that downloaded the web page for the app).
#' connection (as opposed to the request that downloaded the web page for the
#' app).
#' } #' }
#' \item{userData}{ #' \item{userData}{
#' An environment for app authors and module/package authors to store whatever #' An environment for app authors and module/package authors to store whatever
@@ -214,7 +209,7 @@ workerId <- local({
#' Sends a custom message to the web page. `type` must be a #' Sends a custom message to the web page. `type` must be a
#' single-element character vector giving the type of message, while #' single-element character vector giving the type of message, while
#' `message` can be any jsonlite-encodable value. Custom messages #' `message` can be any jsonlite-encodable value. Custom messages
#' have no meaning to Shiny itself; they are used solely to convey information #' have no meaning to Shiny itself; they are used soley to convey information
#' to custom JavaScript logic in the browser. You can do this by adding #' to custom JavaScript logic in the browser. You can do this by adding
#' JavaScript code to the browser that calls #' JavaScript code to the browser that calls
#' \code{Shiny.addCustomMessageHandler(type, function(message){...})} #' \code{Shiny.addCustomMessageHandler(type, function(message){...})}
@@ -362,7 +357,6 @@ ShinySession <- R6Class(
flushCallbacks = 'Callbacks', flushCallbacks = 'Callbacks',
flushedCallbacks = 'Callbacks', flushedCallbacks = 'Callbacks',
inputReceivedCallbacks = 'Callbacks', inputReceivedCallbacks = 'Callbacks',
unhandledErrorCallbacks = 'Callbacks',
bookmarkCallbacks = 'Callbacks', bookmarkCallbacks = 'Callbacks',
bookmarkedCallbacks = 'Callbacks', bookmarkedCallbacks = 'Callbacks',
restoreCallbacks = 'Callbacks', restoreCallbacks = 'Callbacks',
@@ -409,7 +403,7 @@ ShinySession <- R6Class(
sendMessage = function(...) { sendMessage = function(...) {
# This function is a wrapper for $write # This function is a wrapper for $write
msg <- list(...) msg <- list(...)
if (any_unnamed(msg)) { if (anyUnnamed(msg)) {
stop("All arguments to sendMessage must be named.") stop("All arguments to sendMessage must be named.")
} }
private$write(toJSON(msg)) private$write(toJSON(msg))
@@ -484,35 +478,6 @@ ShinySession <- R6Class(
# "json" unless requested otherwise. The only other valid value is # "json" unless requested otherwise. The only other valid value is
# "rds". # "rds".
format <- params$format %||% "json" format <- params$format %||% "json"
# Machines can test their snapshot under different locales.
# R CMD check runs under the `C` locale.
# However, before this parameter, existing snapshots were most likely not
# under the `C` locale is would cause failures. This parameter allows
# users to opt-in to the `C` locale.
# From ?sort:
# However, there are some caveats with the radix sort:
# If x is a character vector, all elements must share the
# same encoding. Only UTF-8 (including ASCII) and Latin-1
# encodings are supported. Collation always follows the "C"
# locale.
# {shinytest2} will always set `sortC=1`
# {shinytest} does not have `sortC` functionality.
# Users should set `options(shiny.snapshotsortc = TRUE)` within their app.
# The sortingMethod should always be `radix` going forward.
sortMethod <-
if (!is.null(params$sortC)) {
if (params$sortC != "1") {
stop("The `sortC` parameter can only be `1` or not supplied")
}
"radix"
} else {
# Allow users to set an option for {shinytest2}.
if (isTRUE(getShinyOption("snapshotsortc", default = FALSE))) {
"radix"
} else {
"auto"
}
}
values <- list() values <- list()
@@ -555,7 +520,7 @@ ShinySession <- R6Class(
} }
) )
values$input <- sortByName(values$input, method = sortMethod) values$input <- sortByName(values$input)
} }
if (!is.null(params$output)) { if (!is.null(params$output)) {
@@ -583,7 +548,7 @@ ShinySession <- R6Class(
} }
) )
values$output <- sortByName(values$output, method = sortMethod) values$output <- sortByName(values$output)
} }
if (!is.null(params$export)) { if (!is.null(params$export)) {
@@ -604,7 +569,7 @@ ShinySession <- R6Class(
) )
} }
values$export <- sortByName(values$export, method = sortMethod) values$export <- sortByName(values$export)
} }
# Make sure input, output, and export are all named lists (at this # Make sure input, output, and export are all named lists (at this
@@ -724,7 +689,6 @@ ShinySession <- R6Class(
private$flushCallbacks <- Callbacks$new() private$flushCallbacks <- Callbacks$new()
private$flushedCallbacks <- Callbacks$new() private$flushedCallbacks <- Callbacks$new()
private$inputReceivedCallbacks <- Callbacks$new() private$inputReceivedCallbacks <- Callbacks$new()
private$unhandledErrorCallbacks <- Callbacks$new()
private$.input <- ReactiveValues$new(dedupe = FALSE, label = "input") private$.input <- ReactiveValues$new(dedupe = FALSE, label = "input")
private$.clientData <- ReactiveValues$new(dedupe = TRUE, label = "clientData") private$.clientData <- ReactiveValues$new(dedupe = TRUE, label = "clientData")
private$timingRecorder <- ShinyServerTimingRecorder$new() private$timingRecorder <- ShinyServerTimingRecorder$new()
@@ -861,7 +825,7 @@ ShinySession <- R6Class(
dots <- eval(substitute(alist(...))) dots <- eval(substitute(alist(...)))
} }
if (any_unnamed(dots)) if (anyUnnamed(dots))
stop("exportTestValues: all arguments must be named.") stop("exportTestValues: all arguments must be named.")
names(dots) <- ns(names(dots)) names(dots) <- ns(names(dots))
@@ -949,7 +913,7 @@ ShinySession <- R6Class(
# Copy `values` from scopeState to state, adding namespace # Copy `values` from scopeState to state, adding namespace
if (length(scopeState$values) != 0) { if (length(scopeState$values) != 0) {
if (any_unnamed(scopeState$values)) { if (anyUnnamed(scopeState$values)) {
stop("All scope values in must be named.") stop("All scope values in must be named.")
} }
@@ -1045,21 +1009,8 @@ ShinySession <- R6Class(
new data from the client." new data from the client."
return(private$inputReceivedCallbacks$register(callback)) return(private$inputReceivedCallbacks$register(callback))
}, },
onUnhandledError = function(callback) { unhandledError = function(e) {
"Registers the callback to be invoked when an unhandled error occurs." self$close()
return(private$unhandledErrorCallbacks$register(callback))
},
unhandledError = function(e, close = TRUE) {
"Call the global and session unhandled error handlers and then close the
session if the error is fatal."
if (close) {
class(e) <- c("shiny.error.fatal", class(e))
}
private$unhandledErrorCallbacks$invoke(e, onError = printError)
.globals$onUnhandledErrorCallbacks$invoke(e, onError = printError)
if (close) self$close()
}, },
close = function() { close = function() {
if (!self$closed) { if (!self$closed) {
@@ -1163,14 +1114,7 @@ ShinySession <- R6Class(
structure(list(), class = "try-error", condition = cond) structure(list(), class = "try-error", condition = cond)
} else if (inherits(cond, "shiny.output.cancel")) { } else if (inherits(cond, "shiny.output.cancel")) {
structure(list(), class = "cancel-output") structure(list(), class = "cancel-output")
} else if (inherits(cond, "shiny.output.progress")) { } else if (inherits(cond, "shiny.silent.error")) {
structure(list(), class = "progress-output")
} else if (cnd_inherits(cond, "shiny.silent.error")) {
# The error condition might have been chained by
# foreign code, e.g. dplyr. Find the original error.
while (!inherits(cond, "shiny.silent.error")) {
cond <- cond$parent
}
# Don't let shiny.silent.error go through the normal stop # Don't let shiny.silent.error go through the normal stop
# path of try, because we don't want it to print. But we # 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 # do want to try to return the same looking result so that
@@ -1183,7 +1127,6 @@ ShinySession <- R6Class(
"logs or contact the app author for", "logs or contact the app author for",
"clarification.")) "clarification."))
} }
self$unhandledError(cond, close = FALSE)
invisible(structure(list(), class = "try-error", condition = cond)) invisible(structure(list(), class = "try-error", condition = cond))
} }
} }
@@ -1194,33 +1137,6 @@ ShinySession <- R6Class(
# client knows that progress is over. # client knows that progress is over.
self$requestFlush() self$requestFlush()
if (inherits(value, "progress-output")) {
# This is the case where an output needs to compute for longer
# than this reactive flush. We put the output into progress mode
# (i.e. adding .recalculating) with a special flag that means
# the progress indication should not be cleared until this
# specific output receives a new value or error.
self$showProgress(name, persistent=TRUE)
# It's conceivable that this output already ran successfully
# within this reactive flush, in which case we could either show
# the new output while simultaneously making it .recalculating;
# or we squelch the new output and make whatever output is in
# the client .recalculating. I (jcheng) decided on the latter as
# it seems more in keeping with what we do with these kinds of
# intermediate output values/errors in general, i.e. ignore them
# and wait until we have a final answer. (Also kind of feels
# like a bug in the app code if you routinely have outputs that
# are executing successfully, only to be invalidated again
# within the same reactive flush--use priority to fix that.)
private$invalidatedOutputErrors$remove(name)
private$invalidatedOutputValues$remove(name)
# It's important that we return so that the existing output in
# the client remains untouched.
return()
}
private$sendMessage(recalculating = list( private$sendMessage(recalculating = list(
name = name, status = 'recalculated' name = name, status = 'recalculated'
)) ))
@@ -1353,29 +1269,23 @@ ShinySession <- R6Class(
private$startCycle() private$startCycle()
} }
}, },
showProgress = function(id, persistent=FALSE) { showProgress = function(id) {
'Send a message to the client that recalculation of the output identified 'Send a message to the client that recalculation of the output identified
by \\code{id} is in progress. There is currently no mechanism for by \\code{id} is in progress. There is currently no mechanism for
explicitly turning off progress for an output component; instead, all explicitly turning off progress for an output component; instead, all
progress is implicitly turned off when flushOutput is next called. progress is implicitly turned off when flushOutput is next called.'
You can use persistent=TRUE if the progress for this output component
should stay on beyond the flushOutput (or any subsequent flushOutputs); in
that case, progress is only turned off (and the persistent flag cleared)
when the output component receives a value or error, or, if
showProgress(id, persistent=FALSE) is called and a subsequent flushOutput
occurs.'
# If app is already closed, be sure not to show progress, otherwise we # If app is already closed, be sure not to show progress, otherwise we
# will get an error because of the closed websocket # will get an error because of the closed websocket
if (self$closed) if (self$closed)
return() return()
if (!id %in% private$progressKeys) { if (id %in% private$progressKeys)
private$progressKeys <- c(private$progressKeys, id) return()
}
self$sendProgress('binding', list(id = id, persistent = persistent)) private$progressKeys <- c(private$progressKeys, id)
self$sendProgress('binding', list(id = id))
}, },
sendProgress = function(type, message) { sendProgress = function(type, message) {
private$sendMessage( private$sendMessage(
@@ -1791,7 +1701,7 @@ ShinySession <- R6Class(
dots <- eval(substitute(alist(...))) dots <- eval(substitute(alist(...)))
} }
if (any_unnamed(dots)) if (anyUnnamed(dots))
stop("exportTestValues: all arguments must be named.") stop("exportTestValues: all arguments must be named.")
# Create a named list where each item is a list with an expression and # Create a named list where each item is a list with an expression and
@@ -1804,7 +1714,7 @@ ShinySession <- R6Class(
}, },
getTestSnapshotUrl = function(input = TRUE, output = TRUE, export = TRUE, getTestSnapshotUrl = function(input = TRUE, output = TRUE, export = TRUE,
format = "json", sortC = FALSE) { format = "json") {
reqString <- function(group, value) { reqString <- function(group, value) {
if (isTRUE(value)) if (isTRUE(value))
paste0(group, "=1") paste0(group, "=1")
@@ -1818,7 +1728,6 @@ ShinySession <- R6Class(
reqString("input", input), reqString("input", input),
reqString("output", output), reqString("output", output),
reqString("export", export), reqString("export", export),
reqString("sortC", sortC),
paste0("format=", format), paste0("format=", format),
sep = "&" sep = "&"
) )
@@ -2024,7 +1933,7 @@ ShinySession <- R6Class(
tmpdata <- tempfile(fileext = ext) tmpdata <- tempfile(fileext = ext)
return(Context$new(getDefaultReactiveDomain(), '[download]')$run(function() { return(Context$new(getDefaultReactiveDomain(), '[download]')$run(function() {
promises::with_promise_domain(reactivePromiseDomain(), { promises::with_promise_domain(reactivePromiseDomain(), {
captureStackTraces({ promises::with_promise_domain(createStackTracePromiseDomain(), {
self$incrementBusyCount() self$incrementBusyCount()
hybrid_chain( hybrid_chain(
# ..stacktraceon matches with the top-level ..stacktraceoff.. # ..stacktraceon matches with the top-level ..stacktraceoff..
@@ -2218,6 +2127,18 @@ ShinySession <- R6Class(
}) })
} }
} }
),
active = list(
session = function() {
shinyDeprecated(
"0.11.1", "shinysession$session",
details = paste0(
"Attempted to access deprecated shinysession$session object. ",
"Please just access the shinysession object directly."
)
)
self
}
) )
) )
@@ -2254,7 +2175,7 @@ ShinySession <- R6Class(
if (getOption("shiny.allowoutputreads", FALSE)) { if (getOption("shiny.allowoutputreads", FALSE)) {
.subset2(x, 'impl')$getOutput(name) .subset2(x, 'impl')$getOutput(name)
} else { } else {
rlang::abort(paste0("Can't read output '", name, "'")) rlang::abort(paste0("Can't read output '", output, "'"))
} }
} }
@@ -2385,89 +2306,23 @@ getCurrentOutputInfo <- function(session = getDefaultReactiveDomain()) {
#' Add callbacks for Shiny session events #' Add callbacks for Shiny session events
#' #'
#' @description
#' These functions are for registering callbacks on Shiny session events. #' These functions are for registering callbacks on Shiny session events.
#' `onFlush` registers a function that will be called before Shiny flushes the #' `onFlush` registers a function that will be called before Shiny flushes
#' reactive system. `onFlushed` registers a function that will be called after #' the reactive system. `onFlushed` registers a function that will be
#' Shiny flushes the reactive system. `onUnhandledError` registers a function to #' called after Shiny flushes the reactive system. `onSessionEnded`
#' be called when an unhandled error occurs before the session is closed. #' registers a function to be called after the client has disconnected.
#' `onSessionEnded` registers a function to be called after the client has
#' disconnected.
#' #'
#' These functions should be called within the application's server function. #' These functions should be called within the application's server function.
#' #'
#' All of these functions return a function which can be called with no #' All of these functions return a function which can be called with no
#' arguments to cancel the registration. #' arguments to cancel the registration.
#' #'
#' @section Unhandled Errors:
#' Unhandled errors are errors that aren't otherwise handled by Shiny or by the
#' application logic. In other words, they are errors that will either cause the
#' application to crash or will result in "Error" output in the UI.
#'
#' You can use `onUnhandledError()` to register a function that will be called
#' when an unhandled error occurs. This function will be called with the error
#' object as its first argument. If the error is fatal and will result in the
#' session closing, the error condition will have the `shiny.error.fatal` class.
#'
#' Note that the `onUnhandledError()` callbacks cannot be used to prevent the
#' app from closing or to modify the error condition. Instead, they are intended
#' to give you an opportunity to log the error or perform other cleanup
#' operations.
#'
#' @param fun A callback function. #' @param fun A callback function.
#' @param once Should the function be run once, and then cleared, or should it #' @param once Should the function be run once, and then cleared, or should it
#' re-run each time the event occurs. (Only for `onFlush` and #' re-run each time the event occurs. (Only for `onFlush` and
#' `onFlushed`.) #' `onFlushed`.)
#' @param session A shiny session object. #' @param session A shiny session object.
#' #'
#' @examplesIf interactive()
#' library(shiny)
#'
#' ui <- fixedPage(
#' markdown(c(
#' "Set the number to 8 or higher to cause an error",
#' "in the `renderText()` output."
#' )),
#' sliderInput("number", "Number", 0, 10, 4),
#' textOutput("text"),
#' hr(),
#' markdown(c(
#' "Click the button below to crash the app with an unhandled error",
#' "in an `observe()` block."
#' )),
#' actionButton("crash", "Crash the app!")
#' )
#'
#' log_event <- function(level, ...) {
#' ts <- strftime(Sys.time(), " [%F %T] ")
#' message(level, ts, ...)
#' }
#'
#' server <- function(input, output, session) {
#' log_event("INFO", "Session started")
#'
#' onUnhandledError(function(err) {
#' # log the unhandled error
#' level <- if (inherits(err, "shiny.error.fatal")) "FATAL" else "ERROR"
#' log_event(level, conditionMessage(err))
#' })
#'
#' onStop(function() {
#' log_event("INFO", "Session ended")
#' })
#'
#' observeEvent(input$crash, stop("Oops, an unhandled error happened!"))
#'
#' output$text <- renderText({
#' if (input$number > 7) {
#' stop("that's too high!")
#' }
#' sprintf("You picked number %d.", input$number)
#' })
#' }
#'
#' shinyApp(ui, server)
#'
#' @export #' @export
onFlush <- function(fun, once = TRUE, session = getDefaultReactiveDomain()) { onFlush <- function(fun, once = TRUE, session = getDefaultReactiveDomain()) {
session$onFlush(fun, once = once) session$onFlush(fun, once = once)
@@ -2488,27 +2343,6 @@ onSessionEnded <- function(fun, session = getDefaultReactiveDomain()) {
session$onSessionEnded(fun) session$onSessionEnded(fun)
} }
.globals$onUnhandledErrorCallbacks <- NULL
on_load({
.globals$onUnhandledErrorCallbacks <- Callbacks$new()
})
#' @rdname onFlush
#' @export
onUnhandledError <- function(fun, session = getDefaultReactiveDomain()) {
if (!is.function(fun) || length(formals(fun)) == 0) {
rlang::abort(
"The unhandled error callback must be a function that takes an error object as its first argument."
)
}
if (is.null(session)) {
.globals$onUnhandledErrorCallbacks$register(fun)
} else {
session$onUnhandledError(fun)
}
}
flushPendingSessions <- function() { flushPendingSessions <- function() {
lapply(appsNeedingFlush$values(), function(shinysession) { lapply(appsNeedingFlush$values(), function(shinysession) {

View File

@@ -162,29 +162,11 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
sharedEnv <- globalenv() sharedEnv <- globalenv()
} }
# To enable hot-reloading of support files, this function is called
# whenever the UI or Server func source is updated. To avoid loading
# support files 2x, we follow the last cache update trigger timestamp.
autoload_r_support_if_needed <- local({
autoload_last_loaded <- -1
function() {
if (!isTRUE(getOption("shiny.autoload.r", TRUE))) return()
last_cache_trigger <- cachedAutoReloadLastChanged$get()
if (identical(autoload_last_loaded, last_cache_trigger)) return()
loadSupport(appDir, renv = sharedEnv, globalrenv = globalenv())
autoload_last_loaded <<- last_cache_trigger
}
})
# uiHandlerSource is a function that returns an HTTP handler for serving up # uiHandlerSource is a function that returns an HTTP handler for serving up
# ui.R as a webpage. The "cachedFuncWithFile" call makes sure that the closure # ui.R as a webpage. The "cachedFuncWithFile" call makes sure that the closure
# we're creating here only gets executed when ui.R's contents change. # we're creating here only gets executed when ui.R's contents change.
uiHandlerSource <- cachedFuncWithFile(appDir, "ui.R", case.sensitive = FALSE, uiHandlerSource <- cachedFuncWithFile(appDir, "ui.R", case.sensitive = FALSE,
function(uiR) { function(uiR) {
autoload_r_support_if_needed()
if (file.exists(uiR)) { if (file.exists(uiR)) {
# If ui.R contains a call to shinyUI (which sets .globals$ui), use that. # If ui.R contains a call to shinyUI (which sets .globals$ui), use that.
# If not, then take the last expression that's returned from ui.R. # If not, then take the last expression that's returned from ui.R.
@@ -211,11 +193,10 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
staticPaths <- list() staticPaths <- list()
} }
fallbackWWWDir <- system_file("www-dir", package = "shiny") fallbackWWWDir <- system.file("www-dir", package = "shiny")
serverSource <- cachedFuncWithFile(appDir, "server.R", case.sensitive = FALSE, serverSource <- cachedFuncWithFile(appDir, "server.R", case.sensitive = FALSE,
function(serverR) { function(serverR) {
autoload_r_support_if_needed()
# If server.R contains a call to shinyServer (which sets .globals$server), # If server.R contains a call to shinyServer (which sets .globals$server),
# use that. If not, then take the last expression that's returned from # use that. If not, then take the last expression that's returned from
# server.R. # server.R.
@@ -251,9 +232,10 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
onStart <- function() { onStart <- function() {
oldwd <<- getwd() oldwd <<- getwd()
setwd(appDir) setwd(appDir)
# TODO: we should support hot reloading on global.R and R/*.R changes.
if (getOption("shiny.autoload.r", TRUE)) { if (getOption("shiny.autoload.r", TRUE)) {
autoload_r_support_if_needed() loadSupport(appDir, renv=sharedEnv, globalrenv=globalenv())
} else { } else {
if (file.exists(file.path.ci(appDir, "global.R"))) if (file.exists(file.path.ci(appDir, "global.R")))
sourceUTF8(file.path.ci(appDir, "global.R")) sourceUTF8(file.path.ci(appDir, "global.R"))
} }
@@ -304,81 +286,37 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
# #
# The return value is a function that halts monitoring when called. # The return value is a function that halts monitoring when called.
initAutoReloadMonitor <- function(dir) { initAutoReloadMonitor <- function(dir) {
if (!get_devmode_option("shiny.autoreload", FALSE)) { if (!getOption("shiny.autoreload", FALSE)) {
return(function(){}) return(function(){})
} }
filePattern <- getOption( filePattern <- getOption("shiny.autoreload.pattern",
"shiny.autoreload.pattern", ".*\\.(r|html?|js|css|png|jpe?g|gif)$")
".*\\.(r|html?|js|css|png|jpe?g|gif)$"
)
lastValue <- NULL
observeLabel <- paste0("File Auto-Reload - '", basename(dir), "'")
obs <- observe(label = observeLabel, {
files <- sort_c(
list.files(dir, pattern = filePattern, recursive = TRUE, ignore.case = TRUE)
)
times <- file.info(files)$mtime
names(times) <- files
if (is_installed("watcher")) { if (is.null(lastValue)) {
check_for_update <- function(paths) { # First run
paths <- grep( lastValue <<- times
filePattern, } else if (!identical(lastValue, times)) {
paths, # We've changed!
ignore.case = TRUE, lastValue <<- times
value = TRUE
)
if (length(paths) == 0) {
return()
}
cachedAutoReloadLastChanged$set()
autoReloadCallbacks$invoke() autoReloadCallbacks$invoke()
} }
# [garrick, 2025-02-20] Shiny <= v1.10.0 used `invalidateLater()` with an invalidateLater(getOption("shiny.autoreload.interval", 500))
# autoreload.interval in ms. {watcher} instead uses a latency parameter in })
# seconds, which serves a similar purpose and that I'm keeping for backcompat.
latency <- getOption("shiny.autoreload.interval", 250) / 1000
watcher <- watcher::watcher(dir, check_for_update, latency = latency)
watcher$start()
onStop(watcher$stop)
} else {
# Fall back to legacy observer behavior
if (!is_false(getOption("shiny.autoreload.legacy_warning", TRUE))) {
cli::cli_warn(
c(
"Using legacy autoreload file watching. Please install {.pkg watcher} for a more performant autoreload file watcher.",
"i" = "Set {.run options(shiny.autoreload.legacy_warning = FALSE)} to suppress this warning."
),
.frequency = "regularly",
.frequency_id = "shiny.autoreload.legacy_warning"
)
}
lastValue <- NULL onStop(obs$destroy)
observeLabel <- paste0("File Auto-Reload - '", basename(dir), "'")
watcher <- observe(label = observeLabel, {
files <- sort_c(
list.files(dir, pattern = filePattern, recursive = TRUE, ignore.case = TRUE)
)
times <- file.info(files)$mtime
names(times) <- files
if (is.null(lastValue)) { obs$destroy
# First run
lastValue <<- times
} else if (!identical(lastValue, times)) {
# We've changed!
lastValue <<- times
cachedAutoReloadLastChanged$set()
autoReloadCallbacks$invoke()
}
invalidateLater(getOption("shiny.autoreload.interval", 500))
})
onStop(watcher$destroy)
watcher$destroy
}
invisible(watcher)
} }
#' Load an app's supporting R files #' Load an app's supporting R files
@@ -401,7 +339,7 @@ initAutoReloadMonitor <- function(dir) {
#' @param appDir The application directory. If `appDir` is `NULL` or #' @param appDir The application directory. If `appDir` is `NULL` or
#' not supplied, the nearest enclosing directory that is a Shiny app, starting #' not supplied, the nearest enclosing directory that is a Shiny app, starting
#' with the current directory, is used. #' with the current directory, is used.
#' @param renv The environment in which the files in the `R/` directory should #' @param renv The environmeny in which the files in the `R/` directory should
#' be evaluated. #' be evaluated.
#' @param globalrenv The environment in which `global.R` should be evaluated. If #' @param globalrenv The environment in which `global.R` should be evaluated. If
#' `NULL`, `global.R` will not be evaluated at all. #' `NULL`, `global.R` will not be evaluated at all.
@@ -413,6 +351,17 @@ loadSupport <- function(appDir=NULL, renv=new.env(parent=globalenv()), globalren
appDir <- findEnclosingApp(".") appDir <- findEnclosingApp(".")
} }
descFile <- file.path.ci(appDir, "DESCRIPTION")
if (file.exists(file.path.ci(appDir, "NAMESPACE")) ||
(file.exists(descFile) &&
identical(as.character(read.dcf(descFile, fields = "Type")), "Package")))
{
warning(
"Loading R/ subdirectory for Shiny application, but this directory appears ",
"to contain an R package. Sourcing files in R/ may cause unexpected behavior."
)
}
if (!is.null(globalrenv)){ if (!is.null(globalrenv)){
# Evaluate global.R, if it exists. # Evaluate global.R, if it exists.
globalPath <- file.path.ci(appDir, "global.R") globalPath <- file.path.ci(appDir, "global.R")
@@ -427,12 +376,10 @@ loadSupport <- function(appDir=NULL, renv=new.env(parent=globalenv()), globalren
helpersDir <- file.path(appDir, "R") helpersDir <- file.path(appDir, "R")
disabled <- list.files(helpersDir, pattern="^_disable_autoload\\.r$", recursive=FALSE, ignore.case=TRUE) disabled <- list.files(helpersDir, pattern="^_disable_autoload\\.r$", recursive=FALSE, ignore.case=TRUE)
if (length(disabled) > 0) { if (length(disabled) > 0){
return(invisible(renv)) return(invisible(renv))
} }
warn_if_app_dir_is_package(appDir)
helpers <- list.files(helpersDir, pattern="\\.[rR]$", recursive=FALSE, full.names=TRUE) helpers <- list.files(helpersDir, pattern="\\.[rR]$", recursive=FALSE, full.names=TRUE)
# Ensure files in R/ are sorted according to the 'C' locale before sourcing. # Ensure files in R/ are sorted according to the 'C' locale before sourcing.
# This convention is based on the default for packages. For details, see: # This convention is based on the default for packages. For details, see:
@@ -447,27 +394,6 @@ loadSupport <- function(appDir=NULL, renv=new.env(parent=globalenv()), globalren
invisible(renv) invisible(renv)
} }
warn_if_app_dir_is_package <- function(appDir) {
has_namespace <- file.exists(file.path.ci(appDir, "NAMESPACE"))
has_desc_pkg <- FALSE
if (!has_namespace) {
descFile <- file.path.ci(appDir, "DESCRIPTION")
has_desc_pkg <-
file.exists(descFile) &&
identical(as.character(read.dcf(descFile, fields = "Type")), "Package")
}
if (has_namespace || has_desc_pkg) {
warning(
"Loading R/ subdirectory for Shiny application, but this directory appears ",
"to contain an R package. Sourcing files in R/ may cause unexpected behavior. ",
"See `?loadSupport` for more details."
)
}
}
# This reads in an app dir for a single-file application (e.g. app.R), and # This reads in an app dir for a single-file application (e.g. app.R), and
# returns a shiny.appobj. # returns a shiny.appobj.
# appDir must be a normalized (absolute) path, not a relative one # appDir must be a normalized (absolute) path, not a relative one
@@ -483,6 +409,8 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
wasDir <- setwd(appDir) wasDir <- setwd(appDir)
on.exit(setwd(wasDir)) on.exit(setwd(wasDir))
# TODO: we should support hot reloading on R/*.R changes.
# In an upcoming version of shiny, this option will go away.
if (getOption("shiny.autoload.r", TRUE)) { if (getOption("shiny.autoload.r", TRUE)) {
# Create a child env which contains all the helpers and will be the shared parent # Create a child env which contains all the helpers and will be the shared parent
# of the ui.R and server.R load. # of the ui.R and server.R load.
@@ -527,7 +455,7 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
staticPaths <- list() staticPaths <- list()
} }
fallbackWWWDir <- system_file("www-dir", package = "shiny") fallbackWWWDir <- system.file("www-dir", package = "shiny")
oldwd <- NULL oldwd <- NULL
monitorHandle <- NULL monitorHandle <- NULL

View File

@@ -14,11 +14,7 @@ NULL
#' # now we can just write "static" content without withMathJax() #' # now we can just write "static" content without withMathJax()
#' div("more math here $$\\sqrt{2}$$") #' div("more math here $$\\sqrt{2}$$")
withMathJax <- function(...) { withMathJax <- function(...) {
path <- paste0( path <- 'https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'
getOption("shiny.mathjax.url", "https://mathjax.rstudio.com/latest/MathJax.js"),
"?",
getOption("shiny.mathjax.config", "config=TeX-AMS-MML_HTMLorMML")
)
tagList( tagList(
tags$head( tags$head(
singleton(tags$script(src = path, type = 'text/javascript')) singleton(tags$script(src = path, type = 'text/javascript'))
@@ -43,7 +39,7 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
# Put the body into the default template # Put the body into the default template
ui <- htmlTemplate( ui <- htmlTemplate(
system_file("template", "default.html", package = "shiny"), system.file("template", "default.html", package = "shiny"),
lang = lang, lang = lang,
body = ui, body = ui,
# this template is a complete HTML document # this template is a complete HTML document
@@ -51,75 +47,48 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
) )
} }
jquery <- function() {
version <- getOption("shiny.jquery.version", 3)
if (version == 3) {
return(htmlDependency(
"jquery", version_jquery,
c(href = "shared"),
script = "jquery.min.js"
))
}
if (version == 1) {
return(htmlDependency(
"jquery", "1.12.4",
c(href = "shared/legacy"),
script = "jquery.min.js"
))
}
stop("Unsupported version of jQuery: ", version)
}
shiny_deps <- c( shiny_deps <- c(
list(jqueryDependency()), list(jquery()),
shinyDependencies() shinyDependencies()
) )
if (testMode) { if (testMode) {
# Add code injection listener if in test mode # Add code injection listener if in test mode
shiny_deps[[length(shiny_deps) + 1]] <- shiny_deps[[length(shiny_deps) + 1]] <-
htmlDependency( htmlDependency("shiny-testmode", shinyPackageVersion(),
"shiny-testmode", c(href="shared"), script = "shiny-testmode.js")
get_package_version("shiny"),
src = "www/shared",
package = "shiny",
script = "shiny-testmode.js",
all_files = FALSE
)
} }
if (in_devmode() || in_client_devmode()) {
# If we're in dev mode, add a simple script to the head that injects a
# global variable for the client to use to detect dev mode.
shiny_deps[[length(shiny_deps) + 1]] <-
htmlDependency(
"shiny-devmode",
get_package_version("shiny"),
src = "www/shared",
package = "shiny",
head="<script>window.__SHINY_DEV_MODE__ = true;</script>",
all_files = FALSE
)
}
html <- renderDocument(ui, shiny_deps, processDep = createWebDependency) html <- renderDocument(ui, shiny_deps, processDep = createWebDependency)
enc2utf8(paste(collapse = "\n", html)) enc2utf8(paste(collapse = "\n", html))
} }
jqueryDependency <- function() {
version <- getOption("shiny.jquery.version", 3)
if (version == 3) {
return(htmlDependency(
"jquery", version_jquery,
src = "www/shared",
package = "shiny",
script = "jquery.min.js",
all_files = FALSE
))
}
if (version == 1) {
return(htmlDependency(
"jquery", "1.12.4",
src = "www/shared/legacy",
package = "shiny",
script = "jquery.min.js",
all_files = FALSE
))
}
stop("Unsupported version of jQuery: ", version)
}
shinyDependencies <- function() { shinyDependencies <- function() {
list( list(
bslib::bs_dependency_defer(shinyDependencyCSS), bslib::bs_dependency_defer(shinyDependencyCSS),
busyIndicatorDependency(),
htmlDependency( htmlDependency(
name = "shiny-javascript", name = "shiny-javascript",
version = get_package_version("shiny"), version = shinyPackageVersion(),
src = "www/shared", src = c(href = "shared"),
package = "shiny",
script = script =
if (isTRUE( if (isTRUE(
get_devmode_option( get_devmode_option(
@@ -129,38 +98,29 @@ shinyDependencies <- function() {
)) ))
"shiny.min.js" "shiny.min.js"
else else
"shiny.js", "shiny.js"
all_files = FALSE
) )
) )
} }
shinyDependencySass <- function(bs_version) {
bootstrap_scss <- paste0("shiny.bootstrap", bs_version, ".scss")
scss_home <- system_file("www/shared/shiny_scss", package = "shiny")
scss_files <- file.path(scss_home, c(bootstrap_scss, "shiny.scss"))
lapply(scss_files, sass::sass_file)
}
shinyDependencyCSS <- function(theme) { shinyDependencyCSS <- function(theme) {
version <- get_package_version("shiny") version <- shinyPackageVersion()
if (!is_bs_theme(theme)) { if (!is_bs_theme(theme)) {
return(htmlDependency( return(htmlDependency(
name = "shiny-css", name = "shiny-css",
version = version, version = version,
src = "www/shared", src = c(href = "shared"),
package = "shiny", stylesheet = "shiny.min.css"
stylesheet = "shiny.min.css",
all_files = FALSE
)) ))
} }
bs_version <- bslib::theme_version(theme) scss_home <- system.file("www/shared/shiny_scss", package = "shiny")
scss_files <- file.path(scss_home, c("bootstrap.scss", "shiny.scss"))
scss_files <- lapply(scss_files, sass::sass_file)
bslib::bs_dependency( bslib::bs_dependency(
input = shinyDependencySass(bs_version), input = scss_files,
theme = theme, theme = theme,
name = "shiny-sass", name = "shiny-sass",
version = version, version = version,
@@ -170,7 +130,7 @@ shinyDependencyCSS <- function(theme) {
#' Create a Shiny UI handler #' Create a Shiny UI handler
#' #'
#' @description `r lifecycle::badge("superseded")` #' @description \lifecycle{superseded}
#' #'
#' @description Historically this function was used in ui.R files to register a user #' @description Historically this function was used in ui.R files to register a user
#' interface with Shiny. It is no longer required as of Shiny 0.10; simply #' interface with Shiny. It is no longer required as of Shiny 0.10; simply
@@ -178,7 +138,7 @@ shinyDependencyCSS <- function(theme) {
#' This function is kept for backwards compatibility with older applications. It #' This function is kept for backwards compatibility with older applications. It
#' returns the value that is passed to it. #' returns the value that is passed to it.
#' #'
#' @param ui A user interface definition #' @param ui A user interace definition
#' @return The user interface definition, without modifications or side effects. #' @return The user interface definition, without modifications or side effects.
#' @keywords internal #' @keywords internal
#' @export #' @export

View File

@@ -2,23 +2,12 @@ utils::globalVariables('func', add = TRUE)
#' Mark a function as a render function #' Mark a function as a render function
#' #'
#' `r lifecycle::badge("superseded")` Please use [`createRenderFunction()`] to
#' support async execution. (Shiny 1.1.0)
#'
#' Should be called by implementers of `renderXXX` functions in order to mark #' Should be called by implementers of `renderXXX` functions in order to mark
#' their return values as Shiny render functions, and to provide a hint to Shiny #' their return values as Shiny render functions, and to provide a hint to Shiny
#' regarding what UI function is most commonly used with this type of render #' regarding what UI function is most commonly used with this type of render
#' function. This can be used in R Markdown documents to create complete output #' function. This can be used in R Markdown documents to create complete output
#' widgets out of just the render function. #' widgets out of just the render function.
#' #'
#' Note that it is generally preferable to use [createRenderFunction()] instead
#' of `markRenderFunction()`. It essentially wraps up the user-provided
#' expression in the `transform` function passed to it, then passes the resulting
#' function to `markRenderFunction()`. It also provides a simpler calling
#' interface. There may be cases where `markRenderFunction()` must be used instead of
#' [createRenderFunction()] -- for example, when the `transform` parameter of
#' [createRenderFunction()] is not flexible enough for your needs.
#'
#' @param uiFunc A function that renders Shiny UI. Must take a single argument: #' @param uiFunc A function that renders Shiny UI. Must take a single argument:
#' an output ID. #' an output ID.
#' @param renderFunc A function that is suitable for assigning to a Shiny output #' @param renderFunc A function that is suitable for assigning to a Shiny output
@@ -48,7 +37,7 @@ utils::globalVariables('func', add = TRUE)
#' is able to serve JS and CSS resources. #' is able to serve JS and CSS resources.
#' @return The `renderFunc` function, with annotations. #' @return The `renderFunc` function, with annotations.
#' #'
#' @seealso [createRenderFunction()] #' @seealso [createRenderFunction()], [quoToFunction()]
#' @export #' @export
markRenderFunction <- function( markRenderFunction <- function(
uiFunc, uiFunc,
@@ -58,12 +47,6 @@ markRenderFunction <- function(
cacheWriteHook = NULL, cacheWriteHook = NULL,
cacheReadHook = NULL cacheReadHook = NULL
) { ) {
# (Do not emit warning for superseded code, "since theres no risk if you keep using it")
# # This method is called by the superseding function, createRenderFunction().
# if (in_devmode()) {
# shinyDeprecated("1.1.0", "markRenderFunction()", "createRenderFunction()")
# }
force(renderFunc) force(renderFunc)
# a mutable object that keeps track of whether `useRenderFunction` has been # a mutable object that keeps track of whether `useRenderFunction` has been
@@ -111,7 +94,6 @@ markRenderFunction <- function(
# For everything else, do nothing. # For everything else, do nothing.
cacheHint <- lapply(cacheHint, function(x) { cacheHint <- lapply(cacheHint, function(x) {
if (is.function(x)) formalsAndBody(x) if (is.function(x)) formalsAndBody(x)
else if (is_quosure(x)) zap_srcref(quo_get_expr(x))
else if (is.language(x)) zap_srcref(x) else if (is.language(x)) zap_srcref(x)
else x else x
}) })
@@ -151,27 +133,10 @@ print.shiny.render.function <- function(x, ...) {
cat_line("<shiny.render.function>") cat_line("<shiny.render.function>")
} }
#' Implement custom render functions #' Implement render functions
#' #'
#' Developer-facing utilities for implementing a custom `renderXXX()` function. #' This function is a wrapper for [markRenderFunction()] which provides support
#' Before using these utilities directly, consider using the [`htmlwidgets` #' for async computation via promises.
#' package](http://www.htmlwidgets.org/develop_intro.html) to implement custom
#' outputs (i.e., custom `renderXXX()`/`xxxOutput()` functions). That said,
#' these utilities can be used more directly if a full-blown htmlwidget isn't
#' needed and/or the user-supplied reactive expression needs to be wrapped in
#' additional call(s).
#'
#' To implement a custom `renderXXX()` function, essentially 2 things are needed:
#' 1. Capture the user's reactive expression as a function.
#' * New `renderXXX()` functions can use `quoToFunction()` for this, but
#' already existing `renderXXX()` functions that contain `env` and `quoted`
#' parameters may want to continue using `installExprFunction()` for better
#' legacy support (see examples).
#' 2. Flag the resulting function (from 1) as a Shiny rendering function and
#' also provide a UI container for displaying the result of the rendering
#' function.
#' * `createRenderFunction()` is currently recommended (instead of
#' [markRenderFunction()]) for this step (see examples).
#' #'
#' @param func A function without parameters, that returns user data. If the #' @param func A function without parameters, that returns user data. If the
#' returned value is a promise, then the render function will proceed in async #' returned value is a promise, then the render function will proceed in async
@@ -188,24 +153,16 @@ print.shiny.render.function <- function(x, ...) {
#' @return An annotated render function, ready to be assigned to an #' @return An annotated render function, ready to be assigned to an
#' `output` slot. #' `output` slot.
#' #'
#' @seealso [quoToFunction()], [markRenderFunction()].
#'
#' @examples #' @examples
#' # A custom render function that repeats the supplied value 3 times #' # A very simple render function
#' renderTriple <- function(expr) { #' renderTriple <- function(x) {
#' # Wrap user-supplied reactive expression into a function #' x <- substitute(x)
#' func <- quoToFunction(rlang::enquo0(expr)) #' if (!rlang::is_quosure(x)) {
#' #' x <- rlang::new_quosure(x, env = parent.frame())
#' createRenderFunction( #' }
#' func, #' func <- quoToFunction(x, "renderTriple")
#' transform = function(value, session, name, ...) {
#' paste(rep(value, 3), collapse=", ")
#' },
#' outputFunc = textOutput
#' )
#' }
#'
#' # For better legacy support, consider using installExprFunction() over quoToFunction()
#' renderTripleLegacy <- function(expr, env = parent.frame(), quoted = FALSE) {
#' func <- installExprFunction(expr, "func", env, quoted)
#' #'
#' createRenderFunction( #' createRenderFunction(
#' func, #' func,
@@ -217,38 +174,10 @@ print.shiny.render.function <- function(x, ...) {
#' } #' }
#' #'
#' # Test render function from the console #' # Test render function from the console
#' reactiveConsole(TRUE) #' a <- 1
#' #' r <- renderTriple({ a + 1 })
#' v <- reactiveVal("basic") #' a <- 2
#' r <- renderTriple({ v() })
#' r() #' r()
#' #> [1] "basic, basic, basic"
#'
#' # User can supply quoted code via rlang::quo(). Note that evaluation of the
#' # expression happens when r2() is invoked, not when r2 is created.
#' q <- rlang::quo({ v() })
#' r2 <- rlang::inject(renderTriple(!!q))
#' v("rlang")
#' r2()
#' #> [1] "rlang, rlang, rlang"
#'
#' # Supplying quoted code without rlang::quo() requires installExprFunction()
#' expr <- quote({ v() })
#' r3 <- renderTripleLegacy(expr, quoted = TRUE)
#' v("legacy")
#' r3()
#' #> [1] "legacy, legacy, legacy"
#'
#' # The legacy approach also supports with quosures (env is ignored in this case)
#' q <- rlang::quo({ v() })
#' r4 <- renderTripleLegacy(q, quoted = TRUE)
#' v("legacy-rlang")
#' r4()
#' #> [1] "legacy-rlang, legacy-rlang, legacy-rlang"
#'
#' # Turn off reactivity in the console
#' reactiveConsole(FALSE)
#'
#' @export #' @export
createRenderFunction <- function( createRenderFunction <- function(
func, func,
@@ -383,13 +312,13 @@ markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
#' The corresponding HTML output tag should be `div` or `img` and have #' The corresponding HTML output tag should be `div` or `img` and have
#' the CSS class name `shiny-image-output`. #' the CSS class name `shiny-image-output`.
#' #'
#' @seealso #' @seealso For more details on how the images are generated, and how to control
#' * For more details on how the images are generated, and how to control
#' the output, see [plotPNG()]. #' the output, see [plotPNG()].
#' * Use [outputOptions()] to set general output options for an image output.
#' #'
#' @param expr An expression that returns a list. #' @param expr An expression that returns a list.
#' @inheritParams renderUI #' @param env The environment in which to evaluate `expr`.
#' @param quoted Is `expr` a quoted expression (with `quote()`)? This
#' is useful if you want to save an expression in a variable.
#' @param deleteFile Should the file in `func()$src` be deleted after #' @param deleteFile Should the file in `func()$src` be deleted after
#' it is sent to the client browser? Generally speaking, if the image is a #' it is sent to the client browser? Generally speaking, if the image is a
#' temp file generated within `func`, then this should be `TRUE`; #' temp file generated within `func`, then this should be `TRUE`;
@@ -468,10 +397,11 @@ markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
#' #'
#' shinyApp(ui, server) #' shinyApp(ui, server)
#' } #' }
renderImage <- function(expr, env = parent.frame(), quoted = FALSE, renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
deleteFile, outputArgs=list()) deleteFile, outputArgs=list())
{ {
func <- installExprFunction(expr, "func", env, quoted, label = "renderImage") expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderImage")
# missing() must be used directly within the function with the given arg # missing() must be used directly within the function with the given arg
if (missing(deleteFile)) { if (missing(deleteFile)) {
@@ -593,19 +523,21 @@ isTemp <- function(path, tempDir = tempdir(), mustExist) {
#' function return [invisible()]. #' function return [invisible()].
#' #'
#' @param expr An expression to evaluate. #' @param expr An expression to evaluate.
#' @inheritParams renderUI #' @param env The environment in which to evaluate `expr`. For expert use only.
#' @param quoted Is `expr` a quoted expression (with `quote()`)? This
#' is useful if you want to save an expression in a variable.
#' @param width Width of printed output. #' @param width Width of printed output.
#' @param outputArgs A list of arguments to be passed through to the implicit #' @param outputArgs A list of arguments to be passed through to the implicit
#' call to [verbatimTextOutput()] or [textOutput()] when the functions are #' call to [verbatimTextOutput()] or [textOutput()] when the functions are
#' used in an interactive RMarkdown document. #' used in an interactive RMarkdown document.
#' #'
#' @example res/text-example.R #' @example res/text-example.R
#' @seealso [outputOptions()]
#' @export #' @export
renderPrint <- function(expr, env = parent.frame(), quoted = FALSE, renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
width = getOption('width'), outputArgs=list()) width = getOption('width'), outputArgs=list())
{ {
func <- installExprFunction(expr, "func", env, quoted, label = "renderPrint") expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderPrint")
# Set a promise domain that sets the console width # Set a promise domain that sets the console width
# and captures output # and captures output
@@ -637,7 +569,7 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
outputArgs, outputArgs,
cacheHint = list( cacheHint = list(
label = "renderPrint", label = "renderPrint",
origUserExpr = installedFuncExpr(func) origUserExpr = get_expr(expr)
) )
) )
} }
@@ -687,10 +619,11 @@ createRenderPrintPromiseDomain <- function(width) {
#' element. #' element.
#' @export #' @export
#' @rdname renderPrint #' @rdname renderPrint
renderText <- function(expr, env = parent.frame(), quoted = FALSE, renderText <- function(expr, env=parent.frame(), quoted=FALSE,
outputArgs=list(), sep=" ") { outputArgs=list(), sep=" ") {
func <- installExprFunction(expr, "func", env, quoted, label = "renderText") expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderText")
createRenderFunction( createRenderFunction(
func, func,
@@ -711,18 +644,14 @@ renderText <- function(expr, env = parent.frame(), quoted = FALSE,
#' #'
#' @param expr An expression that returns a Shiny tag object, [HTML()], #' @param expr An expression that returns a Shiny tag object, [HTML()],
#' or a list of such objects. #' or a list of such objects.
#' @template param-env #' @param env The environment in which to evaluate `expr`.
#' @templateVar x expr #' @param quoted Is `expr` a quoted expression (with `quote()`)? This
#' @templateVar env env #' is useful if you want to save an expression in a variable.
#' @templateVar quoted quoted
#' @template param-quoted
#' @templateVar x expr
#' @templateVar quoted quoted
#' @param outputArgs A list of arguments to be passed through to the implicit #' @param outputArgs A list of arguments to be passed through to the implicit
#' call to [uiOutput()] when `renderUI` is used in an #' call to [uiOutput()] when `renderUI` is used in an
#' interactive R Markdown document. #' interactive R Markdown document.
#' #'
#' @seealso [uiOutput()], [outputOptions()] #' @seealso [uiOutput()]
#' @export #' @export
#' @examples #' @examples
#' ## Only run examples in interactive R sessions #' ## Only run examples in interactive R sessions
@@ -746,7 +675,8 @@ renderText <- function(expr, env = parent.frame(), quoted = FALSE,
renderUI <- function(expr, env = parent.frame(), quoted = FALSE, renderUI <- function(expr, env = parent.frame(), quoted = FALSE,
outputArgs = list()) outputArgs = list())
{ {
func <- installExprFunction(expr, "func", env, quoted, label = "renderUI") expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderUI")
createRenderFunction( createRenderFunction(
func, func,
@@ -781,9 +711,9 @@ renderUI <- function(expr, env = parent.frame(), quoted = FALSE,
#' function.) #' function.)
#' @param contentType A string of the download's #' @param contentType A string of the download's
#' [content type](https://en.wikipedia.org/wiki/Internet_media_type), for #' [content type](https://en.wikipedia.org/wiki/Internet_media_type), for
#' example `"text/csv"` or `"image/png"`. If `NULL`, the content type #' example `"text/csv"` or `"image/png"`. If `NULL` or
#' will be guessed based on the filename extension, or #' `NA`, the content type will be guessed based on the filename
#' `application/octet-stream` if the extension is unknown. #' extension, or `application/octet-stream` if the extension is unknown.
#' @param outputArgs A list of arguments to be passed through to the implicit #' @param outputArgs A list of arguments to be passed through to the implicit
#' call to [downloadButton()] when `downloadHandler` is used #' call to [downloadButton()] when `downloadHandler` is used
#' in an interactive R Markdown document. #' in an interactive R Markdown document.
@@ -812,15 +742,8 @@ renderUI <- function(expr, env = parent.frame(), quoted = FALSE,
#' #'
#' shinyApp(ui, server) #' shinyApp(ui, server)
#' } #' }
#'
#' @seealso
#' * The download handler, like other outputs, is suspended (disabled) by
#' default for download buttons and links that are hidden. Use
#' [outputOptions()] to control this behavior, e.g. to set
#' `suspendWhenHidden = FALSE` if the download is initiated by
#' programmatically clicking on the download button using JavaScript.
#' @export #' @export
downloadHandler <- function(filename, content, contentType=NULL, outputArgs=list()) { downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()) {
renderFunc <- function(shinysession, name, ...) { renderFunc <- function(shinysession, name, ...) {
shinysession$registerDownload(name, filename, contentType, content) shinysession$registerDownload(name, filename, contentType, content)
} }
@@ -832,12 +755,16 @@ downloadHandler <- function(filename, content, contentType=NULL, outputArgs=list
#' Table output with the JavaScript DataTables library #' Table output with the JavaScript DataTables library
#' #'
#' @description #' @description
#' `r lifecycle::badge("deprecated")` #' Makes a reactive version of the given function that returns a data frame (or
#' matrix), which will be rendered with the [DataTables](https://datatables.net)
#' library. Paging, searching, filtering, and sorting can be done on the R side
#' using Shiny as the server infrastructure.
#' #'
#' This function is deprecated, use #' This function only provides the server-side version of DataTables (using R
#' [DT::renderDT()](https://rstudio.github.io/DT/shiny.html) instead. It #' to process the data object on the server side). There is a separate
#' provides a superset of functionality, better performance, and better user #' [DT](https://github.com/rstudio/DT) that allows you to create both
#' experience. #' server-side and client-side DataTables, and supports additional features.
#' Learn more at <https://rstudio.github.io/DT/shiny.html>.
#' #'
#' @param expr An expression that returns a data frame or a matrix. #' @param expr An expression that returns a data frame or a matrix.
#' @inheritParams renderTable #' @inheritParams renderTable
@@ -889,62 +816,21 @@ downloadHandler <- function(filename, content, contentType=NULL, outputArgs=list
#' } #' }
#' ) #' )
#' } #' }
#' @keywords internal
renderDataTable <- function(expr, options = NULL, searchDelay = 500, renderDataTable <- function(expr, options = NULL, searchDelay = 500,
callback = 'function(oTable) {}', escape = TRUE, callback = 'function(oTable) {}', escape = TRUE,
env = parent.frame(), quoted = FALSE, env = parent.frame(), quoted = FALSE,
outputArgs = list()) { outputArgs=list())
{
legacy <- useLegacyDataTable( if (in_devmode()) {
from = "shiny::renderDataTable()", shinyDeprecated(
to = "DT::renderDT()" "0.11.1", "shiny::renderDataTable()", "DT::renderDataTable()",
) details = "See <https://rstudio.github.io/DT/shiny.html> for more information"
if (!quoted) {
expr <- substitute(expr)
quoted <- TRUE
}
if (legacy) {
legacyRenderDataTable(
expr, env = env, quoted = quoted,
options = options,
searchDelay = searchDelay,
callback = callback,
escape = escape,
outputArgs = outputArgs
)
} else {
if (!missing(searchDelay)) {
warning("Ignoring renderDataTable()'s searchDelay value (since DT::renderDT() has no equivalent).")
}
force(options)
force(callback)
force(escape)
force(outputArgs)
DT::renderDataTable(
expr, env = env, quoted = quoted,
options = if (is.null(options)) list() else options,
# Turn function into a statement
callback = DT::JS(paste0("(", callback, ")(table)")),
escape = escape,
outputArgs = outputArgs
) )
} }
}
expr <- get_quosure(expr, env, quoted)
legacyRenderDataTable <- function(expr, options = NULL, searchDelay = 500, func <- quoToFunction(expr, "renderDataTable")
callback = 'function(oTable) {}', escape = TRUE,
env = parent.frame(), quoted = FALSE,
outputArgs=list()) {
func <- installExprFunction(expr, "func", env, quoted, label = "renderDataTable")
renderFunc <- function(shinysession, name, ...) { renderFunc <- function(shinysession, name, ...) {
if (is.function(options)) options <- options() if (is.function(options)) options <- options()
@@ -997,7 +883,7 @@ legacyRenderDataTable <- function(expr, options = NULL, searchDelay = 500,
DT10Names <- function() { DT10Names <- function() {
rbind( rbind(
utils::read.table( utils::read.table(
system_file('www/shared/datatables/upgrade1.10.txt', package = 'shiny'), system.file('www/shared/datatables/upgrade1.10.txt', package = 'shiny'),
stringsAsFactors = FALSE stringsAsFactors = FALSE
), ),
c('aoColumns', 'Removed') # looks like an omission on the upgrade guide c('aoColumns', 'Removed') # looks like an omission on the upgrade guide
@@ -1032,3 +918,67 @@ checkDT9 <- function(options) {
names(options)[i] <- nms10 names(options)[i] <- nms10
options options
} }
# Deprecated functions ------------------------------------------------------
#' Deprecated reactive functions
#'
#' @description \lifecycle{superseded}
#'
#' @name deprecatedReactives
#' @keywords internal
NULL
#' Plot output (deprecated)
#'
#' `reactivePlot` has been replaced by [renderPlot()].
#' @param func A function.
#' @param width Width.
#' @param height Height.
#' @param ... Other arguments to pass on.
#' @rdname deprecatedReactives
#' @export
reactivePlot <- function(func, width='auto', height='auto', ...) {
shinyDeprecated("0.4.0", "reactivePlot()", "renderPlot()")
renderPlot({ func() }, width=width, height=height, ...)
}
#' Table output (deprecated)
#'
#' `reactiveTable` has been replaced by [renderTable()].
#' @rdname deprecatedReactives
#' @export
reactiveTable <- function(func, ...) {
shinyDeprecated("0.4.0", "reactiveTable()", "renderTable()")
renderTable({ func() })
}
#' Print output (deprecated)
#'
#' `reactivePrint` has been replaced by [renderPrint()].
#' @rdname deprecatedReactives
#' @export
reactivePrint <- function(func) {
shinyDeprecated("0.4.0", "reactivePrint()", "renderPrint()")
renderPrint({ func() })
}
#' UI output (deprecated)
#'
#' `reactiveUI` has been replaced by [renderUI()].
#' @rdname deprecatedReactives
#' @export
reactiveUI <- function(func) {
shinyDeprecated("0.4.0", "reactiveUI()", "renderUI()")
renderUI({ func() })
}
#' Text output (deprecated)
#'
#' `reactiveText` has been replaced by [renderText()].
#' @rdname deprecatedReactives
#' @export
reactiveText <- function(func) {
shinyDeprecated("0.4.0", "reactiveText()", "renderText()")
renderText({ func() })
}

View File

@@ -32,40 +32,26 @@ licenseLink <- function(licenseName) {
showcaseHead <- function() { showcaseHead <- function() {
deps <- list( deps <- list(
jqueryuiDependency(), htmlDependency("jqueryui", "1.12.1", c(href="shared/jqueryui"),
htmlDependency( script = "jquery-ui.min.js"),
"showdown", htmlDependency("showdown", "0.3.1", c(href="shared/showdown/compressed"),
"0.3.1", script = "showdown.js"),
src = "www/shared/showdown/compressed", htmlDependency("highlight.js", "6.2", c(href="shared/highlight"),
package="shiny", script = "highlight.pack.js")
script = "showdown.js"
),
htmlDependency(
"highlight.js",
"6.2",
src = "www/shared/highlight",
package="shiny",
script = "highlight.pack.js",
stylesheet = "rstudio.css"
),
htmlDependency(
"showcase",
"0.1.0",
src = "www/shared",
package = "shiny",
script = "shiny-showcase.js",
stylesheet = "shiny-showcase.css",
all_files = FALSE
)
) )
mdfile <- file.path.ci(getwd(), 'Readme.md') mdfile <- file.path.ci(getwd(), 'Readme.md')
html <- tagList( html <- with(tags, tagList(
script(src="shared/shiny-showcase.js"),
link(rel="stylesheet", type="text/css",
href="shared/highlight/rstudio.css"),
link(rel="stylesheet", type="text/css",
href="shared/shiny-showcase.css"),
if (file.exists(mdfile)) if (file.exists(mdfile))
tags$script(type="text/markdown", id="showcase-markdown-content", script(type="text/markdown", id="showcase-markdown-content",
paste(readUTF8(mdfile), collapse="\n")) paste(readUTF8(mdfile), collapse="\n"))
else "" else ""
) ))
return(attachDependencies(html, deps)) return(attachDependencies(html, deps))
} }
@@ -97,7 +83,7 @@ navTabsHelper <- function(files, prefix = "") {
with(tags, with(tags,
li(class=if (tolower(file) %in% c("app.r", "server.r")) "active" else "", li(class=if (tolower(file) %in% c("app.r", "server.r")) "active" else "",
a(href=paste("#", gsub(".", "_", file, fixed=TRUE), "_code", sep=""), a(href=paste("#", gsub(".", "_", file, fixed=TRUE), "_code", sep=""),
"data-toggle"="tab", "data-bs-toggle"="tab", paste0(prefix, file))) "data-toggle"="tab", paste0(prefix, file)))
) )
}) })
} }
@@ -106,7 +92,7 @@ navTabsDropdown <- function(files) {
if (length(files) > 0) { if (length(files) > 0) {
with(tags, with(tags,
li(role="presentation", class="dropdown", li(role="presentation", class="dropdown",
a(class="dropdown-toggle", `data-toggle`="dropdown", `data-bs-toggle`="dropdown", href="#", a(class="dropdown-toggle", `data-toggle`="dropdown", href="#",
role="button", `aria-haspopup`="true", `aria-expanded`="false", role="button", `aria-haspopup`="true", `aria-expanded`="false",
"www", span(class="caret") "www", span(class="caret")
), ),
@@ -148,7 +134,7 @@ showcaseCodeTabs <- function(codeLicense) {
a(id="showcase-code-position-toggle", a(id="showcase-code-position-toggle",
class="btn btn-default btn-sm", class="btn btn-default btn-sm",
onclick="toggleCodePosition()", onclick="toggleCodePosition()",
icon("level-up-alt"), icon("level-up"),
"show with app"), "show with app"),
ul(class="nav nav-tabs", ul(class="nav nav-tabs",
navTabsHelper(rFiles), navTabsHelper(rFiles),

View File

@@ -1,200 +0,0 @@
# Generated by staticimports; do not edit by hand.
# ======================================================================
# Imported from pkg:staticimports
# ======================================================================
# Given a vector, return TRUE if any elements are named, FALSE otherwise.
# For zero-length vectors, always return FALSE.
any_named <- function(x) {
if (length(x) == 0) return(FALSE)
nms <- names(x)
!is.null(nms) && any(nzchar(nms))
}
# Given a vector, return TRUE if any elements are unnamed, FALSE otherwise.
# For zero-length vectors, always return FALSE.
any_unnamed <- function(x) {
if (length(x) == 0) return(FALSE)
nms <- names(x)
is.null(nms) || !all(nzchar(nms))
}
# Borrowed from pkgload:::dev_meta, with some modifications.
# Returns TRUE if `pkg` was loaded with `devtools::load_all()`.
devtools_loaded <- function(pkg) {
ns <- .getNamespace(pkg)
if (is.null(ns) || is.null(ns$.__DEVTOOLS__)) {
return(FALSE)
}
TRUE
}
get_package_version <- function(pkg) {
# `utils::packageVersion()` can be slow, so first try the fast path of
# checking if the package is already loaded.
ns <- .getNamespace(pkg)
if (is.null(ns)) {
utils::packageVersion(pkg)
} else {
as.package_version(ns$.__NAMESPACE__.$spec[["version"]])
}
}
is_installed <- function(pkg, version = NULL) {
installed <- isNamespaceLoaded(pkg) || nzchar(system_file_cached(package = pkg))
if (is.null(version)) {
return(installed)
}
if (!is.character(version) && !inherits(version, "numeric_version")) {
# Avoid https://bugs.r-project.org/show_bug.cgi?id=18548
alert <- if (identical(Sys.getenv("TESTTHAT"), "true")) stop else warning
alert("`version` must be a character string or a `package_version` or `numeric_version` object.")
version <- numeric_version(sprintf("%0.9g", version))
}
installed && isTRUE(get_package_version(pkg) >= version)
}
# Simplified version rlang:::s3_register() that just uses
# warning() instead of rlang::warn() when registration fails
# https://github.com/r-lib/rlang/blob/main/R/compat-s3-register.R
s3_register <- function(generic, class, method = NULL) {
stopifnot(is.character(generic), length(generic) == 1)
stopifnot(is.character(class), length(class) == 1)
pieces <- strsplit(generic, "::")[[1]]
stopifnot(length(pieces) == 2)
package <- pieces[[1]]
generic <- pieces[[2]]
caller <- parent.frame()
get_method_env <- function() {
top <- topenv(caller)
if (isNamespace(top)) {
asNamespace(environmentName(top))
} else {
caller
}
}
get_method <- function(method, env) {
if (is.null(method)) {
get(paste0(generic, ".", class), envir = get_method_env())
} else {
method
}
}
register <- function(...) {
envir <- asNamespace(package)
# Refresh the method each time, it might have been updated by
# `devtools::load_all()`
method_fn <- get_method(method)
stopifnot(is.function(method_fn))
# Only register if generic can be accessed
if (exists(generic, envir)) {
registerS3method(generic, class, method_fn, envir = envir)
} else {
warning(
"Can't find generic `", generic, "` in package ", package,
" register S3 method. Do you need to update ", package,
" to the latest version?", call. = FALSE
)
}
}
# Always register hook in case package is later unloaded & reloaded
setHook(packageEvent(package, "onLoad"), function(...) {
register()
})
# Avoid registration failures during loading (pkgload or regular).
# Check that environment is locked because the registering package
# might be a dependency of the package that exports the generic. In
# that case, the exports (and the generic) might not be populated
# yet (#1225).
if (isNamespaceLoaded(package) && environmentIsLocked(asNamespace(package))) {
register()
}
invisible()
}
# Borrowed from pkgload::shim_system.file, with some modifications. This behaves
# like `system.file()`, except that (1) for packages loaded with
# `devtools::load_all()`, it will return the path to files in the package's
# inst/ directory, and (2) for other packages, the directory lookup is cached.
# Also, to keep the implementation simple, it doesn't support specification of
# lib.loc or mustWork.
system_file <- function(..., package = "base") {
if (!devtools_loaded(package)) {
return(system_file_cached(..., package = package))
}
if (!is.null(names(list(...)))) {
stop("All arguments other than `package` must be unnamed.")
}
# If package was loaded with devtools (the package loaded with load_all),
# also search for files under inst/, and don't cache the results (it seems
# more likely that the package path will change during the development
# process)
pkg_path <- find.package(package)
# First look in inst/
files_inst <- file.path(pkg_path, "inst", ...)
present_inst <- file.exists(files_inst)
# For any files that weren't present in inst/, look in the base path
files_top <- file.path(pkg_path, ...)
present_top <- file.exists(files_top)
# Merge them together. Here are the different possible conditions, and the
# desired result. NULL means to drop that element from the result.
#
# files_inst: /inst/A /inst/B /inst/C /inst/D
# present_inst: T T F F
# files_top: /A /B /C /D
# present_top: T F T F
# result: /inst/A /inst/B /C NULL
#
files <- files_top
files[present_inst] <- files_inst[present_inst]
# Drop cases where not present in either location
files <- files[present_inst | present_top]
if (length(files) == 0) {
return("")
}
# Make sure backslashes are replaced with slashes on Windows
normalizePath(files, winslash = "/")
}
# A wrapper for `system.file()`, which caches the package path because
# `system.file()` can be slow. If a package is not installed, the result won't
# be cached.
system_file_cached <- local({
pkg_dir_cache <- character()
function(..., package = "base") {
if (!is.null(names(list(...)))) {
stop("All arguments other than `package` must be unnamed.")
}
not_cached <- is.na(match(package, names(pkg_dir_cache)))
if (not_cached) {
pkg_dir <- system.file(package = package)
if (nzchar(pkg_dir)) {
pkg_dir_cache[[package]] <<- pkg_dir
}
} else {
pkg_dir <- pkg_dir_cache[[package]]
}
file.path(pkg_dir, ...)
}
})

View File

@@ -158,7 +158,8 @@ print.shiny_runtests <- function(x, ..., reporter = "summary") {
if (any(x$pass)) { if (any(x$pass)) {
cli::cat_bullet("Success", bullet = "tick", bullet_col = "green") # TODO in future... use clisymbols::symbol$tick and crayon green
cat("* Success\n")
mapply( mapply(
x$file, x$file,
x$pass, x$pass,
@@ -170,8 +171,9 @@ print.shiny_runtests <- function(x, ..., reporter = "summary") {
} }
) )
} }
if (!all(x$pass)) { if (any(!x$pass)) {
cli::cat_bullet("Failure", bullet = "cross", bullet_col = "red") # TODO in future... use clisymbols::symbol$cross and crayon red
cat("* Failure\n")
mapply( mapply(
x$file, x$file,
x$pass, x$pass,

View File

@@ -37,11 +37,7 @@
updateTextInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL, placeholder = NULL) { updateTextInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL, placeholder = NULL) {
validate_session_object(session) validate_session_object(session)
message <- dropNulls(list( message <- dropNulls(list(label=label, value=value, placeholder=placeholder))
label = if (!is.null(label)) processDeps(label, session),
value = value,
placeholder = placeholder
))
session$sendInputMessage(inputId, message) session$sendInputMessage(inputId, message)
} }
@@ -115,10 +111,7 @@ updateTextAreaInput <- updateTextInput
updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL) { updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL) {
validate_session_object(session) validate_session_object(session)
message <- dropNulls(list( message <- dropNulls(list(label=label, value=value))
label = if (!is.null(label)) processDeps(label, session),
value = value
))
session$sendInputMessage(inputId, message) session$sendInputMessage(inputId, message)
} }
@@ -126,8 +119,6 @@ updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, l
#' Change the label or icon of an action button on the client #' Change the label or icon of an action button on the client
#' #'
#' @template update-input #' @template update-input
#' @param disabled If `TRUE`, the button will not be clickable; if `FALSE`, it
#' will be.
#' @inheritParams actionButton #' @inheritParams actionButton
#' #'
#' @seealso [actionButton()] #' @seealso [actionButton()]
@@ -157,13 +148,13 @@ updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, l
#' label = "New label", #' label = "New label",
#' icon = icon("calendar")) #' icon = icon("calendar"))
#' #'
#' # Leaves goButton2's label unchanged and #' # Leaves goButton2's label unchaged and
#' # removes its icon #' # removes its icon
#' updateActionButton(session, "goButton2", #' updateActionButton(session, "goButton2",
#' icon = character(0)) #' icon = character(0))
#' #'
#' # Leaves goButton3's icon, if it exists, #' # Leaves goButton3's icon, if it exists,
#' # unchanged and changes its label #' # unchaged and changes its label
#' updateActionButton(session, "goButton3", #' updateActionButton(session, "goButton3",
#' label = "New label 3") #' label = "New label 3")
#' #'
@@ -178,21 +169,16 @@ updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, l
#' } #' }
#' @rdname updateActionButton #' @rdname updateActionButton
#' @export #' @export
updateActionButton <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL, disabled = NULL) { updateActionButton <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL) {
validate_session_object(session) validate_session_object(session)
message <- dropNulls(list( if (!is.null(icon)) icon <- as.character(validateIcon(icon))
label = if (!is.null(label)) processDeps(label, session), message <- dropNulls(list(label=label, icon=icon))
icon = if (!is.null(icon)) processDeps(validateIcon(icon), session),
disabled = disabled
))
session$sendInputMessage(inputId, message) session$sendInputMessage(inputId, message)
} }
#' @rdname updateActionButton #' @rdname updateActionButton
#' @export #' @export
updateActionLink <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL) { updateActionLink <- updateActionButton
updateActionButton(session, inputId=inputId, label=label, icon=icon)
}
#' Change the value of a date input on the client #' Change the value of a date input on the client
@@ -235,12 +221,7 @@ updateDateInput <- function(session = getDefaultReactiveDomain(), inputId, label
min <- dateYMD(min, "min") min <- dateYMD(min, "min")
max <- dateYMD(max, "max") max <- dateYMD(max, "max")
message <- dropNulls(list( message <- dropNulls(list(label=label, value=value, min=min, max=max))
label = if (!is.null(label)) processDeps(label, session),
value = value,
min = min,
max = max
))
session$sendInputMessage(inputId, message) session$sendInputMessage(inputId, message)
} }
@@ -290,7 +271,7 @@ updateDateRangeInput <- function(session = getDefaultReactiveDomain(), inputId,
max <- dateYMD(max, "max") max <- dateYMD(max, "max")
message <- dropNulls(list( message <- dropNulls(list(
label = if (!is.null(label)) processDeps(label, session), label = label,
value = dropNulls(list(start = start, end = end)), value = dropNulls(list(start = start, end = end)),
min = min, min = min,
max = max max = max
@@ -389,16 +370,13 @@ updateNavlistPanel <- updateTabsetPanel
#' } #' }
#' @export #' @export
updateNumericInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL, updateNumericInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL,
min = NULL, max = NULL, step = NULL) { min = NULL, max = NULL, step = NULL) {
validate_session_object(session) validate_session_object(session)
message <- dropNulls(list( message <- dropNulls(list(
label = if (!is.null(label)) processDeps(label, session), label = label, value = formatNoSci(value),
value = formatNoSci(value), min = formatNoSci(min), max = formatNoSci(max), step = formatNoSci(step)
min = formatNoSci(min),
max = formatNoSci(max),
step = formatNoSci(step)
)) ))
session$sendInputMessage(inputId, message) session$sendInputMessage(inputId, message)
} }
@@ -445,23 +423,6 @@ updateSliderInput <- function(session = getDefaultReactiveDomain(), inputId, lab
{ {
validate_session_object(session) validate_session_object(session)
if (!is.null(value)) {
if (!is.null(min) && !is.null(max)) {
# Validate value/min/max together if all three are provided
tryCatch(
validate_slider_value(min, max, value, "updateSliderInput"),
error = function(err) warning(conditionMessage(err), call. = FALSE)
)
} else if (length(value) < 1 || length(value) > 2 || any(is.na(value))) {
# Otherwise ensure basic assumptions about value are met
warning(
"In updateSliderInput(): value must be a single value or a length-2 ",
"vector and cannot contain NA values.",
call. = FALSE
)
}
}
# If no min/max/value is provided, we won't know the # If no min/max/value is provided, we won't know the
# type, and this will return an empty string # type, and this will return an empty string
dataType <- getSliderType(min, max, value) dataType <- getSliderType(min, max, value)
@@ -478,7 +439,7 @@ updateSliderInput <- function(session = getDefaultReactiveDomain(), inputId, lab
} }
message <- dropNulls(list( message <- dropNulls(list(
label = if (!is.null(label)) processDeps(label, session), label = label,
value = formatNoSci(value), value = formatNoSci(value),
min = formatNoSci(min), min = formatNoSci(min),
max = formatNoSci(max), max = formatNoSci(max),
@@ -509,11 +470,7 @@ updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
)) ))
} }
message <- dropNulls(list( message <- dropNulls(list(label = label, options = options, value = selected))
label = if (!is.null(label)) processDeps(label, session),
options = options,
value = selected
))
session$sendInputMessage(inputId, message) session$sendInputMessage(inputId, message)
} }
@@ -666,11 +623,7 @@ updateSelectInput <- function(session = getDefaultReactiveDomain(), inputId, lab
choices <- if (!is.null(choices)) choicesWithNames(choices) choices <- if (!is.null(choices)) choicesWithNames(choices)
if (!is.null(selected)) selected <- as.character(selected) if (!is.null(selected)) selected <- as.character(selected)
options <- if (!is.null(choices)) selectOptions(choices, selected, inputId, FALSE) options <- if (!is.null(choices)) selectOptions(choices, selected, inputId, FALSE)
message <- dropNulls(list( message <- dropNulls(list(label = label, options = options, value = selected))
label = if (!is.null(label)) processDeps(label, session),
options = options,
value = selected
))
session$sendInputMessage(inputId, message) session$sendInputMessage(inputId, message)
} }

View File

@@ -51,199 +51,60 @@ formalsAndBody <- function(x) {
} }
#' @describeIn createRenderFunction convert a quosure to a function. # This function is to be called from functions like `reactive()`, `observe()`,
#' @param q Quosure of the expression `x`. When capturing expressions to create # and the various render functions. It handles the following cases:
#' your quosure, it is recommended to use [`rlang::enquo0()`] to not unquote # - The typical case where x is an unquoted expression, and `env` and `quoted`
#' the object too early. See [`rlang::enquo0()`] for more details. # are not used.
#' @inheritParams installExprFunction # - New-style metaprogramming cases, where rlang::inject() is used to inline a
#' @export # quosure into the AST, as in `inject(reactive(!!x))`.
quoToFunction <- function( # - Old-style metaprogramming cases, where `env` and/or `quoted` are used.
q, #
label = sys.call(-1)[[1]], # Much of the complexity is handling old-style metaprogramming cases. The code
..stacktraceon = FALSE # in this function is more complicated because it needs to look at unevaluated
) { # expressions in the _calling_ function. If this code were put directly in the
func <- quoToSimpleFunction(as_quosure(q)) # calling function, it would look like this:
wrapFunctionLabel(func, updateFunctionLabel(label), ..stacktraceon = ..stacktraceon, dots = FALSE) #
} # if (!missing(env) || !missing(quoted)) {
# deprecatedEnvQuotedMessage()
updateFunctionLabel <- function(label) { # if (!quoted) x <- substitute(x)
badFnName <- "anonymous" # x <- new_quosure(x, env)
if (all(is.language(label))) { #
# Prevent immediately invoked functions like as.language(a()()) # } else {
if (is.language(label) && length(label) > 1) { # x <- substitute(x)
return(badFnName) # if (!is_quosure(x)) {
# x <- new_quosure(x, env = parent.frame())
# }
# }
#
# In the future, the calling functions will not need to have the `env` and
# `quoted` arguments -- `rlang::inject()` and quosures can be used instead.
# Instead of using this function, `get_quosure()`, the caller can instead use
# just the following code:
#
# x <- substitute(x)
# if (!is_quosure(x)) {
# x <- new_quosure(x, env = parent.frame())
# }
#
get_quosure <- function(x, env, quoted) {
if (!eval(substitute(missing(env)), parent.frame()) ||
!eval(substitute(missing(quoted)), parent.frame()))
{
deprecatedEnvQuotedMessage()
if (!quoted) {
x <- eval(substitute(substitute(x)), parent.frame())
} }
label <- deparse(label, width.cutoff = 500L) x <- new_quosure(x, env)
}
label <- as.character(label)
# Prevent function calls that are over one line; (Assignments are hard to perform)
# Prevent immediately invoked functions like "a()()"
if (length(label) > 1 || grepl("(", label, fixed = TRUE)) {
return(badFnName)
}
if (label == "NULL") {
return(badFnName)
}
label
}
quoToSimpleFunction <- function(q) { } else {
# Should not use `new_function(list(), get_expr(q), get_env(q))` as extra logic x <- eval(substitute(substitute(x)), parent.frame())
# is done by rlang to convert the quosure to a function within `as_function(q)`
fun <- as_function(q)
# If the quosure is empty, then the returned function can not be called. # At this point, x can be a quosure if rlang::inject() is used, but the
# https://github.com/r-lib/rlang/issues/1244 # typical case is that x is not a quosure.
if (quo_is_missing(q)) { if (!is_quosure(x)) {
fn_body(fun) <- quote({}) x <- new_quosure(x, env = parent.frame(2L))
}
# `as_function()` returns a function that takes `...`. We need one that takes no
# args.
fn_fmls(fun) <- list()
fun
}
#' Convert an expression to a function
#'
#' `r lifecycle::badge("superseded")` Please use [`installExprFunction()`] for a better
#' debugging experience (Shiny 0.8.0). If the `expr` and `quoted` parameters are not needed, please see
#' [`quoToFunction()`] (Shiny 1.6.0).
#'
#' Similar to [installExprFunction()] but doesn't register debug hooks.
#'
#' @param expr A quoted or unquoted expression, or a quosure.
#' @param env The desired environment for the function. Defaults to the
#' calling environment two steps back.
#' @param quoted Is the expression quoted?
#' @seealso [`installExprFunction()`] for the modern approach to converting an expression to a function
#' @export
#' @keywords internal
exprToFunction <- function(expr, env = parent.frame(), quoted = FALSE) {
# If `expr` is a raw quosure, must say `quoted = TRUE`; (env is ignored)
# If `inject()` a quosure, env is ignored, and quoted should be FALSE (aka ignored).
# Make article of usage
# * (by joe)
if (!quoted) {
expr <- eval(substitute(substitute(expr)), parent.frame())
}
# MUST call with `quoted = TRUE` as exprToQuo() will not reach high enough
q <- exprToQuo(expr, env, quoted = TRUE)
# MUST call `as_function()`. Can NOT call `new_function()`
# rlang has custom logic for handling converting a quosure to a function
quoToSimpleFunction(q)
}
# For internal use only; External users should be using `exprToFunction()` or `installExprFunction()`
# MUST be the exact same logic as `exprToFunction()`, but without the `quoToSimpleFunction()` call
exprToQuo <- function(expr, env = parent.frame(), quoted = FALSE) {
if (!quoted) {
expr <- eval(substitute(substitute(expr)), parent.frame())
}
q <-
if (is_quosure(expr)) {
# inject()ed quosure
# do nothing
expr
} else if (is.language(expr) || rlang::is_atomic(expr) || is.null(expr)) {
# Most common case...
new_quosure(expr, env = env)
} else {
stop("Don't know how to convert '", class(expr)[1], "' to a function; a quosure or quoted expression was expected")
} }
q
}
#' @describeIn createRenderFunction converts a user's reactive `expr` into a
#' function that's assigned to a `name` in the `assign.env`.
#'
#' @param name The name the function should be given
#' @param eval.env The desired environment for the function. Defaults to the
#' calling environment two steps back.
#' @param assign.env The environment in which the function should be assigned.
#' @param label A label for the object to be shown in the debugger. Defaults to
#' the name of the calling function.
#' @param wrappedWithLabel,..stacktraceon Advanced use only. For stack manipulation purposes; see
#' [stacktrace()].
#' @inheritParams exprToFunction
#' @export
installExprFunction <- function(expr, name, eval.env = parent.frame(2),
quoted = FALSE,
assign.env = parent.frame(1),
label = sys.call(-1)[[1]],
wrappedWithLabel = TRUE,
..stacktraceon = FALSE) {
if (!quoted) {
quoted <- TRUE
expr <- eval(substitute(substitute(expr)), parent.frame())
} }
func <- exprToFunction(expr, eval.env, quoted) x
if (length(label) > 1) {
# Just in case the deparsed code is more complicated than we imagine. If we
# have a label with length > 1 it causes warnings in wrapFunctionLabel.
label <- paste0(label, collapse = "\n")
}
wrappedWithLabel <- isTRUE(wrappedWithLabel)
if (wrappedWithLabel) {
func <- wrapFunctionLabel(func, updateFunctionLabel(label), ..stacktraceon = ..stacktraceon, dots = FALSE)
}
assign(name, func, envir = assign.env)
if (!wrappedWithLabel) {
registerDebugHook(name, assign.env, label)
}
invisible(func)
}
# Utility function for creating a debugging label, given an expression.
# `expr` is a quoted expression.
# `function_name` is the name of the calling function.
# `label` is an optional user-provided label. If NULL, it will be inferred.
exprToLabel <- function(expr, function_name, label = NULL) {
srcref <- attr(expr, "srcref", exact = TRUE)
if (is.null(label)) {
label <- rexprSrcrefToLabel(
srcref[[1]],
simpleExprToFunction(expr, function_name)
)
}
if (length(srcref) >= 2) attr(label, "srcref") <- srcref[[2]]
attr(label, "srcfile") <- srcFileOfRef(srcref[[1]])
label
}
simpleExprToFunction <- function(expr, function_name) {
sprintf('%s(%s)', function_name, paste(deparse(expr), collapse='\n'))
}
installedFuncExpr <- function(func) {
fn_body(attr(func, "wrappedFunc", exact = TRUE))
}
funcToLabelBody <- function(func) {
paste(deparse(installedFuncExpr(func)), collapse='\n')
}
funcToLabel <- function(func, functionLabel, label = NULL) {
if (!is.null(label)) return(label)
sprintf(
'%s(%s)',
functionLabel,
funcToLabelBody(func)
)
}
quoToLabelBody <- function(q) {
paste(deparse(quo_get_expr(q)), collapse='\n')
}
quoToLabel <- function(q, functionLabel, label = NULL) {
if (!is.null(label)) return(label)
sprintf(
'%s(%s)',
functionLabel,
quoToLabelBody(q)
)
} }

View File

@@ -1,21 +0,0 @@
# Check if `x` is a tag(), tagList(), or HTML()
# @param strict If `FALSE`, also consider a normal list() of 'tags' to be a tag list.
isTagLike <- function(x, strict = FALSE) {
isTag(x) || isTagList(x, strict = strict) || isTRUE(attr(x, "html"))
}
isTag <- function(x) {
inherits(x, "shiny.tag")
}
isTagList <- function(x, strict = TRUE) {
if (strict) {
return(inherits(x, "shiny.tag.list"))
}
if (!is.list(x)) {
return(FALSE)
}
all(vapply(x, isTagLike, logical(1)))
}

332
R/utils.R
View File

@@ -2,11 +2,6 @@
#' @include map.R #' @include map.R
NULL NULL
# @staticimports pkg:staticimports
# is_installed get_package_version system_file
# s3_register
# any_named any_unnamed
#' Make a random number generator repeatable #' Make a random number generator repeatable
#' #'
#' Given a function that generates random data, returns a wrapped version of #' Given a function that generates random data, returns a wrapped version of
@@ -131,6 +126,34 @@ dropNullsOrEmpty <- function(x) {
x[!vapply(x, nullOrEmpty, FUN.VALUE=logical(1))] 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
if (length(x) == 0) return(FALSE)
nms <- names(x)
# List with no name attribute
if (is.null(nms)) return(TRUE)
# List with name attribute; check for any ""
any(!nzchar(nms))
}
# Given a vector/list, returns a named vector/list (the labels will be blank). # Given a vector/list, returns a named vector/list (the labels will be blank).
asNamed <- function(x) { asNamed <- function(x) {
@@ -150,7 +173,7 @@ empty_named_list <- function() {
# name as elements in a, the element in a is dropped. Also, if there are any # 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. # duplicated names in a or b, only the last one with that name is kept.
mergeVectors <- function(a, b) { mergeVectors <- function(a, b) {
if (any_unnamed(a) || any_unnamed(b)) { if (anyUnnamed(a) || anyUnnamed(b)) {
stop("Vectors must be either NULL or have names for all elements") stop("Vectors must be either NULL or have names for all elements")
} }
@@ -162,27 +185,15 @@ mergeVectors <- function(a, b) {
# Sort a vector by the names of items. If there are multiple items with the # Sort a vector by the names of items. If there are multiple items with the
# same name, preserve the original order of those items. For empty # same name, preserve the original order of those items. For empty
# vectors/lists/NULL, return the original value. # vectors/lists/NULL, return the original value.
sortByName <- function(x, method = "auto") { sortByName <- function(x) {
if (any_unnamed(x)) if (anyUnnamed(x))
stop("All items must be named") stop("All items must be named")
# Special case for empty vectors/lists, and NULL # Special case for empty vectors/lists, and NULL
if (length(x) == 0) if (length(x) == 0)
return(x) return(x)
# Must provide consistent sort order x[order(names(x))]
# https://github.com/rstudio/shinytest/issues/409
# Using a flag in the snapshot url to determine the method
# `method="radix"` uses `C` locale, which is consistent across platforms
# Even if two platforms share `en_us.UTF-8`, they may not sort consistently
# https://blog.zhimingwang.org/macos-lc_collate-hunt
# (macOS) $ LC_ALL=en_US.UTF-8 sort <<<$'python-dev\npython3-dev'
# python-dev
# python3-dev
# (Linux) $ LC_ALL=en_US.UTF-8 sort <<<$'python-dev\npython3-dev'
# python3-dev
# python-dev
x[order(names(x), method = method)]
} }
# Sort a vector. If a character vector, sort using C locale, which is consistent # Sort a vector. If a character vector, sort using C locale, which is consistent
@@ -393,6 +404,164 @@ getContentType <- function(file, defaultType = 'application/octet-stream') {
mime::guess_type(file, unknown = defaultType, subtype = subtype) mime::guess_type(file, unknown = defaultType, subtype = subtype)
} }
# Create a zero-arg function from a quoted expression and environment
# @examples
# makeFunction(body=quote(print(3)))
makeFunction <- function(args = pairlist(), body, env = parent.frame()) {
eval(call("function", args, body), env)
}
#' Convert an expression to a function
#'
#' This is to be called from another function, because it will attempt to get
#' an unquoted expression from two calls back. Note: as of Shiny 1.6.0, it is
#' recommended to use [quoToFunction()] instead.
#'
#' If expr is a quoted expression, then this just converts it to a function.
#' If expr is a function, then this simply returns expr (and prints a
#' deprecation message).
#' If expr was a non-quoted expression from two calls back, then this will
#' quote the original expression and convert it to a function.
#
#' @param expr A quoted or unquoted expression, or a function.
#' @param env The desired environment for the function. Defaults to the
#' calling environment two steps back.
#' @param quoted Is the expression quoted?
#'
#' @examples
#' # Example of a new renderer, similar to renderText
#' # This is something that toolkit authors will do
#' renderTriple <- function(expr, env=parent.frame(), quoted=FALSE) {
#' # Convert expr to a function
#' func <- shiny::exprToFunction(expr, env, quoted)
#'
#' function() {
#' value <- func()
#' paste(rep(value, 3), collapse=", ")
#' }
#' }
#'
#'
#' # Example of using the renderer.
#' # This is something that app authors will do.
#' values <- reactiveValues(A="text")
#'
#' \dontrun{
#' # Create an output object
#' output$tripleA <- renderTriple({
#' values$A
#' })
#' }
#'
#' # At the R console, you can experiment with the renderer using isolate()
#' tripleA <- renderTriple({
#' values$A
#' })
#'
#' isolate(tripleA())
#' # "text, text, text"
#' @export
exprToFunction <- function(expr, env=parent.frame(), quoted=FALSE) {
if (!quoted) {
expr <- eval(substitute(substitute(expr)), parent.frame())
}
# expr is a quoted expression
makeFunction(body=expr, env=env)
}
#' Install an expression as a function
#'
#' Installs an expression in the given environment as a function, and registers
#' debug hooks so that breakpoints may be set in the function. Note: as of
#' Shiny 1.6.0, it is recommended to use [quoToFunction()] instead.
#'
#' This function can replace `exprToFunction` as follows: we may use
#' `func <- exprToFunction(expr)` if we do not want the debug hooks, or
#' `installExprFunction(expr, "func")` if we do. Both approaches create a
#' function named `func` in the current environment.
#'
#' @seealso Wraps [exprToFunction()]; see that method's documentation
#' for more documentation and examples.
#'
#' @param expr A quoted or unquoted expression
#' @param name The name the function should be given
#' @param eval.env The desired environment for the function. Defaults to the
#' calling environment two steps back.
#' @param quoted Is the expression quoted?
#' @param assign.env The environment in which the function should be assigned.
#' @param label A label for the object to be shown in the debugger. Defaults to
#' the name of the calling function.
#' @param wrappedWithLabel,..stacktraceon Advanced use only. For stack manipulation purposes; see
#' [stacktrace()].
#' @export
installExprFunction <- function(expr, name, eval.env = parent.frame(2),
quoted = FALSE,
assign.env = parent.frame(1),
label = deparse(sys.call(-1)[[1]]),
wrappedWithLabel = TRUE,
..stacktraceon = FALSE) {
if (!quoted) {
quoted <- TRUE
expr <- eval(substitute(substitute(expr)), parent.frame())
}
func <- exprToFunction(expr, eval.env, quoted)
if (length(label) > 1) {
# Just in case the deparsed code is more complicated than we imagine. If we
# have a label with length > 1 it causes warnings in wrapFunctionLabel.
label <- paste0(label, collapse = "\n")
}
if (wrappedWithLabel) {
func <- wrapFunctionLabel(func, label, ..stacktraceon = ..stacktraceon)
} else {
registerDebugHook(name, assign.env, label)
}
assign(name, func, envir = assign.env)
}
#' Convert a quosure to a function for a Shiny render function
#'
#' This takes a quosure and label, and wraps them into a function that should be
#' passed to [createRenderFunction()] or [markRenderFunction()].
#'
#' This function was added in Shiny 1.6.0. Previously, it was recommended to use
#' [installExprFunction()] or [exprToFunction()] in render functions, but now we
#' recommend using [quoToFunction()], because it does not require `env` and
#' `quoted` arguments -- that information is captured by quosures provided by
#' \pkg{rlang}.
#'
#' @param q A quosure.
#' @inheritParams installExprFunction
#' @seealso [createRenderFunction()] for example usage.
#'
#' @export
quoToFunction <- function(q, label, ..stacktraceon = FALSE) {
q <- as_quosure(q)
# Use new_function() instead of as_function(), because as_function() adds an
# extra parent environment. (This may not actually be a problem, though.)
func <- new_function(NULL, get_expr(q), get_env(q))
wrapFunctionLabel(func, label, ..stacktraceon = ..stacktraceon)
}
# Utility function for creating a debugging label, given an expression.
# `expr` is a quoted expression.
# `function_name` is the name of the calling function.
# `label` is an optional user-provided label. If NULL, it will be inferred.
exprToLabel <- function(expr, function_name, label = NULL) {
srcref <- attr(expr, "srcref", exact = TRUE)
if (is.null(label)) {
label <- rexprSrcrefToLabel(
srcref[[1]],
sprintf('%s(%s)', function_name, paste(deparse(expr), collapse = '\n'))
)
}
if (length(srcref) >= 2) attr(label, "srcref") <- srcref[[2]]
attr(label, "srcfile") <- srcFileOfRef(srcref[[1]])
label
}
#' Parse a GET query string from a URL #' Parse a GET query string from a URL
#' #'
#' Returns a named list of key-value pairs. #' Returns a named list of key-value pairs.
@@ -484,7 +653,7 @@ shinyCallingHandlers <- function(expr) {
withCallingHandlers(captureStackTraces(expr), withCallingHandlers(captureStackTraces(expr),
error = function(e) { error = function(e) {
# Don't intercept shiny.silent.error (i.e. validation errors) # Don't intercept shiny.silent.error (i.e. validation errors)
if (cnd_inherits(e, "shiny.silent.error")) if (inherits(e, "shiny.silent.error"))
return() return()
handle <- getOption('shiny.error') handle <- getOption('shiny.error')
@@ -493,6 +662,7 @@ shinyCallingHandlers <- function(expr) {
) )
} }
#' Register a function with the debugger (if one is active). #' Register a function with the debugger (if one is active).
#' #'
#' Call this function after exprToFunction to give any active debugger a hook #' Call this function after exprToFunction to give any active debugger a hook
@@ -770,45 +940,22 @@ formatNoSci <- function(x) {
format(x, scientific = FALSE, digits = 15) format(x, scientific = FALSE, digits = 15)
} }
# A simple getter/setting to track the last time the auto-reload process
# updated. This value is used by `cachedFuncWithFile()` when auto-reload is
# enabled to reload app/ui/server files when watched supporting files change.
cachedAutoReloadLastChanged <- local({
last_update <- 0
list(
set = function() {
last_update <<- as.integer(Sys.time())
invisible(last_update)
},
get = function() {
last_update
}
)
})
# Returns a function that calls the given func and caches the result for # Returns a function that calls the given func and caches the result for
# subsequent calls, unless the given file's mtime changes. # subsequent calls, unless the given file's mtime changes.
cachedFuncWithFile <- function(dir, file, func, case.sensitive = FALSE) { cachedFuncWithFile <- function(dir, file, func, case.sensitive = FALSE) {
dir <- normalizePath(dir, mustWork = TRUE) dir <- normalizePath(dir, mustWork=TRUE)
mtime <- NA
value <- NULL value <- NULL
last_mtime_file <- NA
last_autoreload <- 0
function(...) { function(...) {
fname <- if (case.sensitive) { fname <- if (case.sensitive)
file.path(dir, file) file.path(dir, file)
} else { else
file.path.ci(dir, file) file.path.ci(dir, file)
}
now <- file.info(fname)$mtime now <- file.info(fname)$mtime
autoreload <- last_autoreload < cachedAutoReloadLastChanged$get() if (!identical(mtime, now)) {
if (autoreload || !identical(last_mtime_file, now)) {
value <<- func(fname, ...) value <<- func(fname, ...)
last_mtime_file <<- now mtime <<- now
last_autoreload <<- cachedAutoReloadLastChanged$get()
} }
value value
} }
@@ -1011,7 +1158,7 @@ reactiveStop <- function(message = "", class = NULL) {
#' #'
#' ui <- fluidPage( #' ui <- fluidPage(
#' checkboxGroupInput('in1', 'Check some letters', choices = head(LETTERS)), #' checkboxGroupInput('in1', 'Check some letters', choices = head(LETTERS)),
#' selectizeInput('in2', 'Select a state', choices = c("", state.name)), #' selectizeInput('in2', 'Select a state', choices = state.name),
#' plotOutput('plot') #' plotOutput('plot')
#' ) #' )
#' #'
@@ -1029,7 +1176,7 @@ reactiveStop <- function(message = "", class = NULL) {
#' #'
#' } #' }
validate <- function(..., errorClass = character(0)) { validate <- function(..., errorClass = character(0)) {
results <- sapply(list2(...), function(x) { results <- sapply(list(...), function(x) {
# Detect NULL or NA # Detect NULL or NA
if (is.null(x)) if (is.null(x))
return(NA_character_) return(NA_character_)
@@ -1115,7 +1262,7 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
#' #'
#' You can use `req(FALSE)` (i.e. no condition) if you've already performed #' You can use `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 #' all the checks you needed to by that point and just want to stop the reactive
#' chain now. There is no advantage to this, except perhaps ease of readability #' 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 #' if you have a complicated condition to check for (or perhaps if you'd like to
#' divide your condition into nested `if` statements). #' divide your condition into nested `if` statements).
#' #'
@@ -1137,10 +1284,7 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
#' @param ... Values to check for truthiness. #' @param ... Values to check for truthiness.
#' @param cancelOutput If `TRUE` and an output is being evaluated, stop #' @param cancelOutput If `TRUE` and an output is being evaluated, stop
#' processing as usual but instead of clearing the output, leave it in #' processing as usual but instead of clearing the output, leave it in
#' whatever state it happens to be in. If `"progress"`, do the same as `TRUE`, #' whatever state it happens to be in.
#' but also keep the output in recalculating state; this is intended for cases
#' when an in-progress calculation will not be completed in this reactive
#' flush cycle, but is still expected to provide a result in the future.
#' @return The first value that was passed in. #' @return The first value that was passed in.
#' @export #' @export
#' @examples #' @examples
@@ -1172,8 +1316,6 @@ req <- function(..., cancelOutput = FALSE) {
if (!isTruthy(item)) { if (!isTruthy(item)) {
if (isTRUE(cancelOutput)) { if (isTRUE(cancelOutput)) {
cancelOutput() cancelOutput()
} else if (identical(cancelOutput, "progress")) {
reactiveStop(class = "shiny.output.progress")
} else { } else {
reactiveStop(class = "validation") reactiveStop(class = "validation")
} }
@@ -1267,12 +1409,14 @@ dotloop <- function(fun_, ...) {
#' @param x An expression whose truthiness value we want to determine #' @param x An expression whose truthiness value we want to determine
#' @export #' @export
isTruthy <- function(x) { isTruthy <- function(x) {
if (is.null(x))
return(FALSE)
if (inherits(x, 'try-error')) if (inherits(x, 'try-error'))
return(FALSE) return(FALSE)
if (!is.atomic(x)) if (!is.atomic(x))
return(TRUE) return(TRUE)
if (is.null(x))
return(FALSE)
if (length(x) == 0) if (length(x) == 0)
return(FALSE) return(FALSE)
if (all(is.na(x))) if (all(is.na(x)))
@@ -1452,37 +1596,21 @@ dateYMD <- function(date = NULL, argName = "value") {
# function which calls the original function using the specified name. This can # function which calls the original function using the specified name. This can
# be helpful for profiling, because the specified name will show up on the stack # be helpful for profiling, because the specified name will show up on the stack
# trace. # trace.
wrapFunctionLabel <- function(func, name, ..stacktraceon = FALSE, dots = TRUE) { wrapFunctionLabel <- function(func, name, ..stacktraceon = FALSE) {
if (name == "name" || name == "func" || name == "relabelWrapper") { if (name == "name" || name == "func" || name == "relabelWrapper") {
stop("Invalid name for wrapFunctionLabel: ", name) stop("Invalid name for wrapFunctionLabel: ", name)
} }
if (nchar(name, "bytes") > 10000) {
# Max variable length in R is 10000 bytes. Truncate to a shorter number of
# chars because some characters could be multi-byte.
name <- substr(name, 1, 5000)
}
assign(name, func, environment()) assign(name, func, environment())
registerDebugHook(name, environment(), name) registerDebugHook(name, environment(), name)
if (isTRUE(dots)) { if (..stacktraceon) {
if (..stacktraceon) { # We need to wrap the `...` in `!!quote(...)` so that R CMD check won't
# We need to wrap the `...` in `!!quote(...)` so that R CMD check won't # complain about "... may be used in an incorrect context"
# complain about "... may be used in an incorrect context" body <- expr({ ..stacktraceon..((!!name)(!!quote(...))) })
body <- expr({ ..stacktraceon..((!!name)(!!quote(...))) })
} else {
body <- expr({ (!!name)(!!quote(...)) })
}
relabelWrapper <- new_function(pairlist2(... =), body, environment())
} else { } else {
# Same logic as when `dots = TRUE`, but without the `...` body <- expr({ (!!name)(!!quote(...)) })
if (..stacktraceon) {
body <- expr({ ..stacktraceon..((!!name)()) })
} else {
body <- expr({ (!!name)() })
}
relabelWrapper <- new_function(list(), body, environment())
} }
relabelWrapper <- new_function(pairlist2(... =), body, environment())
# Preserve the original function that was passed in; is used for caching. # Preserve the original function that was passed in; is used for caching.
attr(relabelWrapper, "wrappedFunc") <- func attr(relabelWrapper, "wrappedFunc") <- func
@@ -1736,20 +1864,24 @@ findEnclosingApp <- function(path = ".") {
} }
} }
# Until `rlang::cnd_inherits()` is on CRAN # Check if a package is installed, and if version is specified,
cnd_inherits <- function(cnd, class) { # that we have at least that version
cnd_some(cnd, ~ inherits(.x, class)) is_available <- function(package, version = NULL) {
} installed <- nzchar(system.file(package = package))
cnd_some <- function(.cnd, .p, ...) { if (is.null(version)) {
.p <- rlang::as_function(.p) return(installed)
while (rlang::is_condition(.cnd)) {
if (.p(.cnd, ...)) {
return(TRUE)
}
.cnd <- .cnd$parent
} }
installed && isTRUE(utils::packageVersion(package) >= version)
FALSE
} }
# cached version of utils::packageVersion("shiny")
shinyPackageVersion <- local({
version <- NULL
function() {
if (is.null(version)) {
version <<- utils::packageVersion("shiny")
}
version
}
})

View File

@@ -1,2 +0,0 @@
# Generated by tools/updateBootstrapDatepicker.R; do not edit by hand
version_bs_date_picker <- "1.10.0"

View File

@@ -1,2 +0,0 @@
# Generated by tools/updateIonRangeSlider.R; do not edit by hand
version_ion_range_slider <- "2.3.1"

View File

@@ -1,2 +1,2 @@
# Generated by tools/updatejQuery.R; do not edit by hand # Generated by tools/updatejQuery.R; do not edit by hand
version_jquery <- "3.7.1" version_jquery <- "3.6.0"

View File

@@ -1,2 +0,0 @@
# Generated by tools/updatejQueryUI.R; do not edit by hand
version_jqueryui <- "1.14.1"

View File

@@ -1,2 +0,0 @@
# Generated by tools/updateSelectize.R; do not edit by hand
version_selectize <- "0.15.2"

View File

@@ -1,2 +0,0 @@
# Generated by tools/updateStrftime.R; do not edit by hand
version_strftime <- "0.9.2"

View File

@@ -2,8 +2,8 @@
<!-- badges: start --> <!-- badges: start -->
[![CRAN](https://www.r-pkg.org/badges/version/shiny)](https://CRAN.R-project.org/package=shiny) [![CRAN](https://www.r-pkg.org/badges/version/shiny)](https://CRAN.R-project.org/package=shiny)
[![R build status](https://github.com/rstudio/shiny/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/rstudio/shiny/actions) [![R build status](https://github.com/rstudio/shiny/workflows/R-CMD-check/badge.svg)](https://github.com/rstudio/shiny/actions)
[![RStudio community](https://img.shields.io/badge/community-shiny-blue?style=social&logo=rstudio&logoColor=75AADB)](https://forum.posit.co/new-topic?category=shiny&tags=shiny) [![RStudio community](https://img.shields.io/badge/community-shiny-blue?style=social&logo=rstudio&logoColor=75AADB)](https://community.rstudio.com/new-topic?category=shiny&tags=shiny)
<!-- badges: end --> <!-- badges: end -->
@@ -16,7 +16,7 @@ Easily build rich and productive interactive web apps in R &mdash; no HTML/CSS/J
* A prebuilt set of highly sophisticated, customizable, and easy-to-use widgets (e.g., plots, tables, sliders, dropdowns, date pickers, and more). * A prebuilt set of highly sophisticated, customizable, and easy-to-use widgets (e.g., plots, tables, sliders, dropdowns, date pickers, and more).
* An attractive default look based on [Bootstrap](https://getbootstrap.com/) which can also be easily customized with the [bslib](https://github.com/rstudio/bslib) package or avoided entirely with more direct R bindings to HTML/CSS/JavaScript. * An attractive default look based on [Bootstrap](https://getbootstrap.com/) which can also be easily customized with the [bslib](https://github.com/rstudio/bslib) package or avoided entirely with more direct R bindings to HTML/CSS/JavaScript.
* Seamless integration with [R Markdown](https://shiny.rstudio.com/articles/interactive-docs.html), making it easy to embed numerous applications natively within a larger dynamic document. * Seamless integration with [R Markdown](https://shiny.rstudio.com/articles/interactive-docs.html), making it easy to embed numerous applications natively within a larger dynamic document.
* Tools for improving and monitoring performance, including native support for [async programming](https://posit.co/blog/shiny-1-1-0/), [caching](https://talks.cpsievert.me/20201117), [load testing](https://rstudio.github.io/shinyloadtest/), and more. * Tools for improving and monitoring performance, including native support for [async programming](https://blog.rstudio.com/2018/06/26/shiny-1-1-0/), [caching](https://talks.cpsievert.me/20201117), [load testing](https://rstudio.github.io/shinyloadtest/), and [more](https://support.rstudio.com/hc/en-us/articles/231874748-Scaling-and-Performance-Tuning-in-RStudio-Connect).
* [Modules](https://shiny.rstudio.com/articles/modules.html): a framework for reducing code duplication and complexity. * [Modules](https://shiny.rstudio.com/articles/modules.html): a framework for reducing code duplication and complexity.
* An ability to [bookmark application state](https://shiny.rstudio.com/articles/bookmarking-state.html) and/or [generate code to reproduce output(s)](https://github.com/rstudio/shinymeta). * An ability to [bookmark application state](https://shiny.rstudio.com/articles/bookmarking-state.html) and/or [generate code to reproduce output(s)](https://github.com/rstudio/shinymeta).
* A rich ecosystem of extension packages for more [custom widgets](http://www.htmlwidgets.org/), [input validation](https://github.com/rstudio/shinyvalidate), [unit testing](https://github.com/rstudio/shinytest), and more. * A rich ecosystem of extension packages for more [custom widgets](http://www.htmlwidgets.org/), [input validation](https://github.com/rstudio/shinyvalidate), [unit testing](https://github.com/rstudio/shinytest), and more.
@@ -45,24 +45,16 @@ For more examples and inspiration, check out the [Shiny User Gallery](https://sh
For help with learning fundamental Shiny programming concepts, check out the [Mastering Shiny](https://mastering-shiny.org/) book and the [Shiny Tutorial](https://shiny.rstudio.com/tutorial/). The former is currently more up-to-date with modern Shiny features, whereas the latter takes a deeper, more visual, dive into fundamental concepts. For help with learning fundamental Shiny programming concepts, check out the [Mastering Shiny](https://mastering-shiny.org/) book and the [Shiny Tutorial](https://shiny.rstudio.com/tutorial/). The former is currently more up-to-date with modern Shiny features, whereas the latter takes a deeper, more visual, dive into fundamental concepts.
## Join the conversation
If you want to chat about Shiny, meet other developers, or help us decide what to work on next, [join us on Discord](https://discord.com/invite/yMGCamUMnS).
## Getting Help ## Getting Help
To ask a question about Shiny, please use the [RStudio Community website](https://forum.posit.co/new-topic?category=shiny&tags=shiny). To ask a question about Shiny, please use the [RStudio Community website](https://community.rstudio.com/new-topic?category=shiny&tags=shiny).
For bug reports, please use the [issue tracker](https://github.com/rstudio/shiny/issues) and also keep in mind that by [writing a good bug report](https://github.com/rstudio/shiny/wiki/Writing-Good-Bug-Reports), you're more likely to get help with your problem. For bug reports, please use the [issue tracker](https://github.com/rstudio/shiny/issues) and also keep in mind that by [writing a good bug report](https://github.com/rstudio/shiny/wiki/Writing-Good-Bug-Reports), you're more likely to get help with your problem.
## Contributing ## Contributing
We welcome contributions to the **shiny** package. Please see our [CONTRIBUTING.md](https://github.com/rstudio/shiny/blob/main/.github/CONTRIBUTING.md) file for detailed guidelines of how to contribute. We welcome contributions to the **shiny** package. Please see our [CONTRIBUTING.md](https://github.com/rstudio/shiny/blob/master/.github/CONTRIBUTING.md) file for detailed guidelines of how to contribute.
## License ## License
The shiny package as a whole is licensed under the GPLv3. See the [LICENSE](LICENSE) file for more details. The shiny package as a whole is licensed under the GPLv3. See the [LICENSE](LICENSE) file for more details.
## R version support
Shiny is supported on the latest release version of R, as well as the previous four minor release versions of R. For example, if the latest release R version is 4.3, then that version is supported, as well as 4.2, 4.1, 4.0, 3.6.

View File

@@ -1,40 +0,0 @@
## revdepcheck results
We checked 1278 reverse dependencies (1277 from CRAN + 1 from Bioconductor), comparing R CMD check results across CRAN and dev versions of shiny.
* We saw 2 new problems (NOTEs only)
* We failed to check 19 packages due to installation issues
Issues with CRAN packages are summarised below.
### New problems
R CMD check displayed NOTEs for two packages, unrelated to changes in shiny.
* HH
checking installed package size ... NOTE
* PopED
checking installed package size ... NOTE
### Failed to check
* animalEKF
* AovBay
* Certara.VPCResults
* chipPCR
* ctsem
* dartR.sim
* diveR
* gap
* jsmodule
* loon.shiny
* robmedExtra
* rstanarm
* SensMap
* Seurat
* shinyTempSignal
* Signac
* statsr
* TestAnaAPP
* tidyvpc

View File

@@ -1,108 +0,0 @@
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import prettier from "eslint-plugin-prettier";
import unicorn from "eslint-plugin-unicorn";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default [{
ignores: ["**/*.d.ts"],
}, ...compat.extends(
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
), {
plugins: {
"@typescript-eslint": typescriptEslint,
prettier,
unicorn,
},
languageOptions: {
globals: {
...globals.browser,
Atomics: "readonly",
SharedArrayBuffer: "readonly",
},
parser: tsParser,
ecmaVersion: 2021,
sourceType: "module",
parserOptions: {
project: ["./tsconfig.json"],
},
},
rules: {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "error",
"default-case": ["error"],
"linebreak-style": ["error", "unix"],
quotes: ["error", "double", "avoid-escape"],
semi: ["error", "always"],
"dot-location": ["error", "property"],
camelcase: ["off"],
"unicorn/filename-case": ["error", {
case: "camelCase",
}],
"@typescript-eslint/array-type": ["error", {
default: "array-simple",
readonly: "array-simple",
}],
"@typescript-eslint/consistent-indexed-object-style": ["error", "index-signature"],
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/naming-convention": ["error", {
selector: "default",
format: ["camelCase"],
}, {
selector: "method",
modifiers: ["private"],
format: ["camelCase"],
leadingUnderscore: "require",
}, {
selector: "method",
modifiers: ["protected"],
format: ["camelCase"],
leadingUnderscore: "require",
}, {
selector: "variable",
format: ["camelCase"],
trailingUnderscore: "forbid",
leadingUnderscore: "forbid",
}, {
selector: "parameter",
format: ["camelCase"],
trailingUnderscore: "allow",
leadingUnderscore: "forbid",
}, {
selector: ["enum", "enumMember"],
format: ["PascalCase"],
}, {
selector: "typeLike",
format: ["PascalCase"],
custom: {
regex: "(t|T)ype$",
match: false,
},
}],
},
}];

View File

@@ -0,0 +1,2 @@
library(shinytest)
expect_pass(testApp("../", suffix = osName()))

View File

@@ -0,0 +1,12 @@
app <- ShinyDriver$new("../../")
app$snapshotInit("mytest")
app$snapshot()
{{
if (isTRUE(module)) {
'
app$setInputs(`examplemodule1-button` = "click")
app$setInputs(`examplemodule1-button` = "click")
app$snapshot()'
}
}}

View File

@@ -1 +1,9 @@
shinytest2::test_app() library(testthat)
test_dir(
"./testthat",
# Run in the app's environment containing all support methods.
env = shiny::loadSupport(),
# Display the regular progress output and throw an error if any test error is found
reporter = c("progress", "fail")
)

View File

@@ -1,2 +0,0 @@
# Load application support files into testing environment
shinytest2::load_app_env()

View File

@@ -14,4 +14,5 @@ if (isTRUE(rdir)) {
expect_equal(output$sequence, "1 2 3 4 5 6 7 8 9 10 11 12") expect_equal(output$sequence, "1 2 3 4 5 6 7 8 9 10 11 12")
' '
} }
}}}) }}
})

View File

@@ -1,18 +0,0 @@
library(shinytest2)
test_that("Initial snapshot values are consistent", {
app <- AppDriver$new(name = "init")
app$expect_values()
}){{
if (isTRUE(module)) {
shiny::HTML('
test_that("Module values are consistent", {
app <- AppDriver$new(name = "mod")
app$click("examplemodule1-button")
app$click("examplemodule1-button")
app$expect_values()
})')
}
}}

View File

@@ -1,154 +0,0 @@
<mxfile host="app.diagrams.net" modified="2024-05-07T22:40:15.581Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" etag="Zsitjb4PT-sW3A63SWd7" version="24.3.1" type="device">
<diagram name="Page-1" id="zz6aoPEyabkTD7ESu8ts">
<mxGraphModel dx="595" dy="889" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-1" value="Initial" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="120" y="270" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-2" value="Running" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="270" y="270" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-3" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-1" target="DS1AFzV_2DL1v2c9v1jZ-2" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="260" y="480" as="sourcePoint" />
<mxPoint x="310" y="270" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-4" value="Recalculating" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="210" y="250" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-6" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-2" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="320" y="220" as="sourcePoint" />
<mxPoint x="310" y="350" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-7" value="Idle" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="270" y="350" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-8" value="Recalculated" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="330" y="310" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-9" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-7" target="DS1AFzV_2DL1v2c9v1jZ-10" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="320" y="320" as="sourcePoint" />
<mxPoint x="310" y="440" as="targetPoint" />
<Array as="points">
<mxPoint x="320" y="410" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-10" value="Value" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="280" y="440" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-11" value="Error" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="370" y="440" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-12" value="Persistent" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="90" y="440" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-13" value="Cancel" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="180" y="440" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-14" value="&lt;span style=&quot;text-align: start; font-size: 10pt; font-family: Arial;&quot; data-sheets-userformat=&quot;{&amp;quot;2&amp;quot;:513,&amp;quot;3&amp;quot;:{&amp;quot;1&amp;quot;:0},&amp;quot;12&amp;quot;:0}&quot; data-sheets-value=&quot;{&amp;quot;1&amp;quot;:2,&amp;quot;2&amp;quot;:&amp;quot;{progress: {type: \&amp;quot;binding\&amp;quot;, message: {persistent: true}}}&amp;quot;}&quot; data-sheets-root=&quot;1&quot;&gt;{progress: {type: &quot;binding&quot;, message: {persistent: true}}}&lt;/span&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="45" y="340" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-15" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-10" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="320" y="400" as="sourcePoint" />
<mxPoint x="310" y="550" as="targetPoint" />
<Array as="points">
<mxPoint x="320" y="520" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-16" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-11" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="320" y="490" as="sourcePoint" />
<mxPoint x="320" y="550" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-17" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-12" target="DS1AFzV_2DL1v2c9v1jZ-18" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="330" y="500" as="sourcePoint" />
<mxPoint x="290" y="540" as="targetPoint" />
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-18" value="Invalidated" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="260" y="550" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-20" value="" style="curved=1;endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-18" target="DS1AFzV_2DL1v2c9v1jZ-2" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="260" y="480" as="sourcePoint" />
<mxPoint x="310" y="430" as="targetPoint" />
<Array as="points">
<mxPoint x="420" y="610" />
<mxPoint x="550" y="470" />
<mxPoint x="440" y="320" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-23" value="Recalculating" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="450" y="340" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-24" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-2" target="DS1AFzV_2DL1v2c9v1jZ-12" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="320" y="400" as="sourcePoint" />
<mxPoint x="320" y="450" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-25" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0.395;entryY=-0.025;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-7" target="DS1AFzV_2DL1v2c9v1jZ-11" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="330" y="410" as="sourcePoint" />
<mxPoint x="330" y="460" as="targetPoint" />
<Array as="points">
<mxPoint x="380" y="410" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-26" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-7" target="DS1AFzV_2DL1v2c9v1jZ-13" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="340" y="420" as="sourcePoint" />
<mxPoint x="340" y="470" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-27" value="Value" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="270" y="400" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-28" value="Error" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="330" y="400" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-29" value="No message" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="200" y="400" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-30" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-13" target="DS1AFzV_2DL1v2c9v1jZ-18" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="230" y="490" as="sourcePoint" />
<mxPoint x="300" y="558" as="targetPoint" />
<Array as="points">
<mxPoint x="240" y="520" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-31" value="&lt;span style=&quot;font-family: Arial; font-size: 13px; text-align: left; white-space: pre-wrap; background-color: rgb(255, 255, 255);&quot;&gt;{progress: {type: &quot;binding&quot;}}&lt;/span&gt;" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
<mxGeometry x="190" y="490" width="180" height="30" as="geometry" />
</mxCell>
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-35" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;Shiny output progress states&lt;/h1&gt;&lt;p&gt;This diagram depicts a state machine of output binding progress state. Each node represents a possible state and each edge represents a server-&amp;gt;client message that moves outputs from one state to another. &lt;b&gt;If a node is highlighted in blue&lt;/b&gt;, then the output should be showing a busy state when visible (i.e., &lt;font face=&quot;Courier New&quot;&gt;binding.showProgress(true)&lt;/font&gt;)&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" parent="1" vertex="1">
<mxGeometry x="85" y="120" width="465" height="120" as="geometry" />
</mxCell>
<mxCell id="J9lKobNiy15ndT9nfcn--1" value="" style="curved=1;endArrow=classic;html=1;rounded=0;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-7" target="DS1AFzV_2DL1v2c9v1jZ-18">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="280" y="480" as="sourcePoint" />
<mxPoint x="220" y="510" as="targetPoint" />
<Array as="points">
<mxPoint x="610" y="420" />
</Array>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 312 KiB

View File

@@ -1,6 +0,0 @@
Title: Hello Shiny!
Author: RStudio, Inc.
AuthorUrl: http://www.rstudio.com/
License: MIT
Tags: getting-started
Type: Shiny

View File

@@ -1,3 +0,0 @@
This small Shiny application demonstrates Shiny's automatic UI updates.
Move the *Number of bins* slider and notice how the `renderPlot` expression is automatically re-evaluated when its dependant, `input$bins`, changes, causing a histogram with a new number of bins to be rendered.

View File

@@ -1,54 +0,0 @@
library(shiny)
library(bslib)
# Define UI for app that draws a histogram ----
ui <- page_sidebar(
# App title ----
title = "Hello Shiny!",
# Sidebar panel for inputs ----
sidebar = sidebar(
# Input: Slider for the number of bins ----
sliderInput(
inputId = "bins",
label = "Number of bins:",
min = 1,
max = 50,
value = 30
)
),
# Output: Histogram ----
plotOutput(outputId = "distPlot")
)
# Define server logic required to draw a histogram ----
server <- function(input, output) {
# Histogram of the Old Faithful Geyser Data ----
# with requested number of bins
# This expression that generates a histogram is wrapped in a call
# to renderPlot to indicate that:
#
# 1. It is "reactive" and therefore should be automatically
# re-executed when inputs (input$bins) change
# 2. Its output type is a plot
output$distPlot <- renderPlot({
x <- faithful$waiting
bins <- seq(min(x), max(x), length.out = input$bins + 1)
hist(
x,
breaks = bins,
col = "#75AADB",
border = "white",
xlab = "Waiting time to next eruption (in mins)",
main = "Histogram of waiting times"
)
})
}
# Create Shiny app ----
shinyApp(ui = ui, server = server)

View File

@@ -1,6 +0,0 @@
Title: Shiny Text
Author: RStudio, Inc.
AuthorUrl: http://www.rstudio.com/
License: MIT
Tags: getting-started
Type: Shiny

View File

@@ -1 +0,0 @@
This example demonstrates output of raw text from R using the `renderPrint` function in `server` and the `verbatimTextOutput` function in `ui`. In this case, a textual summary of the data is shown using R's built-in `summary` function.

Some files were not shown because too many files have changed in this diff Show More