mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-11 07:58:11 -05:00
Compare commits
423 Commits
joe/bugfix
...
always-gui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d582e53f73 | ||
|
|
52ad7d12cb | ||
|
|
10810308f0 | ||
|
|
4ce1058448 | ||
|
|
0db06df77f | ||
|
|
fdca53d4d2 | ||
|
|
8395598328 | ||
|
|
1b8635db32 | ||
|
|
60db1e02b0 | ||
|
|
a86e9c3609 | ||
|
|
6d77b22f97 | ||
|
|
e1b3756166 | ||
|
|
edf354f416 | ||
|
|
954a979a83 | ||
|
|
fe9a87fb06 | ||
|
|
1842a15f74 | ||
|
|
a568238472 | ||
|
|
fa200022c5 | ||
|
|
a6347341e3 | ||
|
|
c41481e488 | ||
|
|
767abc3c0c | ||
|
|
e005c24fbf | ||
|
|
8580f544fc | ||
|
|
2daa8ec944 | ||
|
|
2b92014ea5 | ||
|
|
f540679513 | ||
|
|
d165cc6e8e | ||
|
|
c1878fe54f | ||
|
|
f05948629e | ||
|
|
3e37dab4a1 | ||
|
|
6584e1f960 | ||
|
|
64c5a67a0e | ||
|
|
aea4e560ea | ||
|
|
12554a0004 | ||
|
|
83336ef9a5 | ||
|
|
08ab21b50e | ||
|
|
5628346ae1 | ||
|
|
b165127d20 | ||
|
|
905e2238d4 | ||
|
|
47bb1f657c | ||
|
|
c917d18d67 | ||
|
|
93568cd53f | ||
|
|
6af06559f4 | ||
|
|
43239a0485 | ||
|
|
e05f4097d6 | ||
|
|
35e62eaee9 | ||
|
|
858c2e66e6 | ||
|
|
0d156171d4 | ||
|
|
b57cb6c8e1 | ||
|
|
5ddff1bd37 | ||
|
|
036f923e05 | ||
|
|
130f4764a7 | ||
|
|
c4b5e5f8a2 | ||
|
|
ecb21df941 | ||
|
|
71d11ec103 | ||
|
|
213f0d3a93 | ||
|
|
8948eca0f3 | ||
|
|
aa0c841aff | ||
|
|
a8449382f0 | ||
|
|
5b27d9258e | ||
|
|
2590cf3895 | ||
|
|
a9f7068b2f | ||
|
|
1f9e4929a6 | ||
|
|
d56afca33e | ||
|
|
8fa023b4ec | ||
|
|
d9f73c4c6d | ||
|
|
68cf1c5410 | ||
|
|
a70220c6c4 | ||
|
|
99207d1d8f | ||
|
|
0baf2ecd70 | ||
|
|
2c6f830223 | ||
|
|
98eb1b596d | ||
|
|
145d222653 | ||
|
|
67e54572a8 | ||
|
|
3cc9b33a8d | ||
|
|
12bc94fbc0 | ||
|
|
b2379bfa5b | ||
|
|
f4fc13fc2f | ||
|
|
95081c43a7 | ||
|
|
bb3b3d5a47 | ||
|
|
f635f98ccb | ||
|
|
eef44295db | ||
|
|
5e1afc61c1 | ||
|
|
8edcbb3dc1 | ||
|
|
dca3722cb8 | ||
|
|
7eb0e93731 | ||
|
|
6034c3ff7a | ||
|
|
4eeb4a12a7 | ||
|
|
6daa689888 | ||
|
|
cded44b40a | ||
|
|
290c9f6b20 | ||
|
|
be3d712fdf | ||
|
|
f5666bcba1 | ||
|
|
f3c89bed01 | ||
|
|
9b0f170730 | ||
|
|
74350cd443 | ||
|
|
61aa7bb3b0 | ||
|
|
82fdbeda49 | ||
|
|
196b220faf | ||
|
|
f41c484913 | ||
|
|
a1a20b3f4b | ||
|
|
bbf9bee28e | ||
|
|
24a1ef9594 | ||
|
|
c5adef0a05 | ||
|
|
508c197446 | ||
|
|
473ec834fe | ||
|
|
66968904bf | ||
|
|
f169792e59 | ||
|
|
39a23af138 | ||
|
|
d8715819dc | ||
|
|
12444807e8 | ||
|
|
92077d47a1 | ||
|
|
4f54276e1b | ||
|
|
ac30848019 | ||
|
|
921650f53b | ||
|
|
72d81e8a85 | ||
|
|
5c5974106d | ||
|
|
c2cbd3a127 | ||
|
|
8e5aedec00 | ||
|
|
13965acb37 | ||
|
|
8a99b9d401 | ||
|
|
f739a1d476 | ||
|
|
87dd00be13 | ||
|
|
8cd393597a | ||
|
|
b7366ef672 | ||
|
|
3d6329dee8 | ||
|
|
2171420e0c | ||
|
|
e44a9b1ded | ||
|
|
bde5a88295 | ||
|
|
11babd5567 | ||
|
|
4c35d483bc | ||
|
|
d049558728 | ||
|
|
8eed42387c | ||
|
|
5b3366f35a | ||
|
|
fea7397c3b | ||
|
|
4a33582482 | ||
|
|
1bad0553b7 | ||
|
|
ac0b723bb0 | ||
|
|
39454a6c09 | ||
|
|
569157aded | ||
|
|
d2d7770c76 | ||
|
|
5da846f1ce | ||
|
|
713c9ec923 | ||
|
|
b3369616d2 | ||
|
|
082b8ef080 | ||
|
|
0fb9226a9b | ||
|
|
bb55f45d94 | ||
|
|
5b12980b7a | ||
|
|
493ef59dda | ||
|
|
b42d835cbf | ||
|
|
d1d177f80f | ||
|
|
433e5814ed | ||
|
|
2bf9f42b49 | ||
|
|
65efb573bd | ||
|
|
26a701215d | ||
|
|
3be7a20f40 | ||
|
|
6f8092f5a4 | ||
|
|
652fcfe799 | ||
|
|
d7d03ee6a8 | ||
|
|
dc6335ed4d | ||
|
|
b421f6bd7f | ||
|
|
d4358e0793 | ||
|
|
a8dfa0771f | ||
|
|
6df3ce4b19 | ||
|
|
8f40f8cab8 | ||
|
|
0d5a2cee58 | ||
|
|
8db4f41fa9 | ||
|
|
b85b03583b | ||
|
|
28e18fe87b | ||
|
|
2c1961acd7 | ||
|
|
04386f1a5e | ||
|
|
9c915e52ca | ||
|
|
6b6ab48377 | ||
|
|
bf36d07670 | ||
|
|
7166192143 | ||
|
|
509f0790db | ||
|
|
67a776a39a | ||
|
|
d3701df4e6 | ||
|
|
0195e34a7b | ||
|
|
0aa49c8a93 | ||
|
|
437de58922 | ||
|
|
fc76cf21fb | ||
|
|
23d1b25c46 | ||
|
|
8bfb59875f | ||
|
|
36e866743d | ||
|
|
d35c6e35ce | ||
|
|
e9afd8c99e | ||
|
|
43b7c41c4f | ||
|
|
921f60475e | ||
|
|
58433cda01 | ||
|
|
ed5eca5496 | ||
|
|
eff4a1f23e | ||
|
|
9f72b15fcf | ||
|
|
8069ff2b05 | ||
|
|
10deddf2f0 | ||
|
|
3ad1c4076d | ||
|
|
943f31e117 | ||
|
|
c43bc195e7 | ||
|
|
92b1e8f256 | ||
|
|
985970d320 | ||
|
|
5eabaa5207 | ||
|
|
7f60ecc725 | ||
|
|
7c635e1283 | ||
|
|
4727a7adf4 | ||
|
|
8940f14dde | ||
|
|
2fd0ce1a09 | ||
|
|
638bcc0f85 | ||
|
|
d411da3114 | ||
|
|
0acae46835 | ||
|
|
61cc61d9aa | ||
|
|
194320d163 | ||
|
|
902bfb8628 | ||
|
|
b25d72f698 | ||
|
|
a4d8f541dd | ||
|
|
6aaf2ff4d5 | ||
|
|
b0f77d6591 | ||
|
|
f2885dafd2 | ||
|
|
b0725e0153 | ||
|
|
4ce62034ce | ||
|
|
7d4c0ad611 | ||
|
|
d189cd9f23 | ||
|
|
f61ba70bb9 | ||
|
|
6e48692637 | ||
|
|
f7b1bc0e5c | ||
|
|
a213d6f7e1 | ||
|
|
a7d793ecf9 | ||
|
|
5d25481f66 | ||
|
|
77a8a783de | ||
|
|
0492eb7958 | ||
|
|
d37feea299 | ||
|
|
ffb9ad2094 | ||
|
|
1e63dfc4c5 | ||
|
|
051cc51d4b | ||
|
|
56dd92fee8 | ||
|
|
51b835b57f | ||
|
|
ccd7342986 | ||
|
|
82decaa070 | ||
|
|
d1e808d090 | ||
|
|
7aad389338 | ||
|
|
7e07c460de | ||
|
|
81a8ec3ce1 | ||
|
|
800f0a216d | ||
|
|
dade7dc069 | ||
|
|
b271d0a9a2 | ||
|
|
5daa0bc38e | ||
|
|
22665dc9b4 | ||
|
|
a99f11fb10 | ||
|
|
81824575e6 | ||
|
|
f6d010056a | ||
|
|
ffd20bcc6e | ||
|
|
55eaaa869d | ||
|
|
c2e66ca474 | ||
|
|
62b848c2e2 | ||
|
|
dbb657bd91 | ||
|
|
de871b79b0 | ||
|
|
146a6d459d | ||
|
|
9fb1dd18a7 | ||
|
|
9ae894d9e3 | ||
|
|
56e0fbdb05 | ||
|
|
e6325629a9 | ||
|
|
9a3329acc7 | ||
|
|
75ab225d84 | ||
|
|
883668ac93 | ||
|
|
c5f2dece49 | ||
|
|
b55bc5318e | ||
|
|
a39450c2b2 | ||
|
|
b784068701 | ||
|
|
bac4e68b89 | ||
|
|
20e95a4cab | ||
|
|
96da457db3 | ||
|
|
37b8715cff | ||
|
|
7aa3a243ba | ||
|
|
f2b549f9cd | ||
|
|
bc58dba0ad | ||
|
|
8ef9be5290 | ||
|
|
d95560db09 | ||
|
|
98f64df738 | ||
|
|
8634e372da | ||
|
|
cbda7633e0 | ||
|
|
8f00cf50ca | ||
|
|
acca28075b | ||
|
|
9a563463dd | ||
|
|
1ede94b09e | ||
|
|
7f41a54c4e | ||
|
|
6cfab79ce9 | ||
|
|
f33b3c7eef | ||
|
|
93d78ae2b0 | ||
|
|
c498b02289 | ||
|
|
30b62e6f18 | ||
|
|
ec18ef651b | ||
|
|
aad23686fa | ||
|
|
1c85ecd7c0 | ||
|
|
a1ff765235 | ||
|
|
a30ba9226d | ||
|
|
980a1e53a7 | ||
|
|
00092cd2a8 | ||
|
|
53ddb54936 | ||
|
|
ea1e307a51 | ||
|
|
17bc1e2e06 | ||
|
|
ed8f3b730b | ||
|
|
0e109d5237 | ||
|
|
f672226a3d | ||
|
|
481dccd085 | ||
|
|
9612f1c3c8 | ||
|
|
9e1e5f61a3 | ||
|
|
99a566f473 | ||
|
|
1012307467 | ||
|
|
b729f45eaf | ||
|
|
cab799e6ee | ||
|
|
a06322d155 | ||
|
|
d836cb2a2c | ||
|
|
2249c7a28a | ||
|
|
0d0422c0a7 | ||
|
|
5ea556ee77 | ||
|
|
a34496663b | ||
|
|
abeaa71d8d | ||
|
|
281a427718 | ||
|
|
03ab966cdc | ||
|
|
3dcb810346 | ||
|
|
d6eef8b3e1 | ||
|
|
a770f1cbf2 | ||
|
|
a5687df9b4 | ||
|
|
202881cbbd | ||
|
|
24ac3b9d8b | ||
|
|
47c1fb88b9 | ||
|
|
170b143b17 | ||
|
|
3854b49c35 | ||
|
|
bae4f604b5 | ||
|
|
36f32e14d3 | ||
|
|
9e521e6927 | ||
|
|
4176f541fc | ||
|
|
18f2afbf85 | ||
|
|
f9a94d9758 | ||
|
|
d1e672e3e3 | ||
|
|
202b924e63 | ||
|
|
4c3342aa99 | ||
|
|
b1e5dd1d1d | ||
|
|
d43ebfbdb9 | ||
|
|
cfadd8307b | ||
|
|
fa6cf9832d | ||
|
|
0075b0da33 | ||
|
|
59c6367cb5 | ||
|
|
194323a9ee | ||
|
|
1ca437e4ee | ||
|
|
77e43b9f50 | ||
|
|
a23f4b0224 | ||
|
|
0541c90980 | ||
|
|
b73a263de8 | ||
|
|
d7ffee68cf | ||
|
|
89cd42b904 | ||
|
|
404185eb8c | ||
|
|
8c29a81b12 | ||
|
|
9b42c6c379 | ||
|
|
03c2dd9e4b | ||
|
|
d8274c3d8c | ||
|
|
85b5fb090f | ||
|
|
2adef311ed | ||
|
|
7050d0b8ad | ||
|
|
8358144a4f | ||
|
|
44e083e0a9 | ||
|
|
a5418cf6ee | ||
|
|
c74630d6eb | ||
|
|
157d4ac9a9 | ||
|
|
8228613c01 | ||
|
|
b907e17b70 | ||
|
|
aa7000427c | ||
|
|
8562c90454 | ||
|
|
5a9a04cd72 | ||
|
|
6b32611356 | ||
|
|
770ebc394f | ||
|
|
c0d35e84b1 | ||
|
|
5e74478864 | ||
|
|
46852e2051 | ||
|
|
b9dded0bef | ||
|
|
6d05f403a5 | ||
|
|
8368634f85 | ||
|
|
8d57d909b4 | ||
|
|
9b7855d597 | ||
|
|
6a5e1b9998 | ||
|
|
af6e558699 | ||
|
|
26d4dddffd | ||
|
|
e2765b4881 | ||
|
|
9796b25f33 | ||
|
|
01b8d3a314 | ||
|
|
50c48de0de | ||
|
|
bfc90da054 | ||
|
|
9d8d6fd6b1 | ||
|
|
43344d9a78 | ||
|
|
01a593c857 | ||
|
|
1b2dd11a4c | ||
|
|
d90a2c4801 | ||
|
|
f065c21ee6 | ||
|
|
d0324bd497 | ||
|
|
e57fba07db | ||
|
|
549425cb81 | ||
|
|
6023165268 | ||
|
|
ffedf29db4 | ||
|
|
00219d342a | ||
|
|
1281ba18cd | ||
|
|
f1f2fae420 | ||
|
|
5809070b05 | ||
|
|
2c492540ce | ||
|
|
61556b505d | ||
|
|
d6a4bc87e8 | ||
|
|
e49f4696e6 | ||
|
|
4219f50141 | ||
|
|
6a4c8556a3 | ||
|
|
488f1c8b83 | ||
|
|
1d7a913d29 | ||
|
|
f89131205d | ||
|
|
d616cf045b | ||
|
|
42af54ca04 | ||
|
|
9f55cd46d8 | ||
|
|
5956f6b123 | ||
|
|
8c6a830521 | ||
|
|
9142cf19c0 | ||
|
|
887b7fb34a | ||
|
|
1392547783 | ||
|
|
735b9b8c7a | ||
|
|
cba974ec34 | ||
|
|
421d588a2f | ||
|
|
8b848277d2 | ||
|
|
8ae19c7243 |
@@ -12,7 +12,7 @@
|
||||
^\.travis\.yml$
|
||||
^staticdocs$
|
||||
^tools$
|
||||
^srcjs$
|
||||
^srcts$
|
||||
^CONTRIBUTING.md$
|
||||
^cran-comments.md$
|
||||
^.*\.o$
|
||||
|
||||
136
.github/workflows/R-CMD-check.yaml
vendored
136
.github/workflows/R-CMD-check.yaml
vendored
@@ -45,38 +45,52 @@ jobs:
|
||||
- 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: Query dependencies
|
||||
run: |
|
||||
install.packages('remotes')
|
||||
saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2)
|
||||
- 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
|
||||
if: runner.os != 'Windows'
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.R_LIBS_USER }}
|
||||
key: ${{ matrix.config.os }}-r-${{ matrix.config.r }}-1-${{ hashFiles('.github/depends.Rds') }}
|
||||
restore-keys: ${{ matrix.config.os }}-r-${{ matrix.config.r }}-1-
|
||||
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'
|
||||
env:
|
||||
RHUB_PLATFORM: linux-x86_64-ubuntu-gcc
|
||||
shell: Rscript {0}
|
||||
run: |
|
||||
Rscript -e "remotes::install_github('r-hub/sysreqs')"
|
||||
sysreqs=$(Rscript -e "cat(sysreqs::sysreq_commands('DESCRIPTION'))")
|
||||
sudo -s eval "$sysreqs"
|
||||
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: |
|
||||
remotes::install_deps(dependencies = TRUE)
|
||||
remotes::install_cran("rcmdcheck")
|
||||
pak::local_install_dev_deps(upgrade = TRUE)
|
||||
pak::pkg_install("rcmdcheck")
|
||||
shell: Rscript {0}
|
||||
|
||||
- name: Find PhantomJS path
|
||||
@@ -84,11 +98,11 @@ jobs:
|
||||
run: |
|
||||
echo "::set-output name=path::$(Rscript -e 'cat(shinytest:::phantom_paths()[[1]])')"
|
||||
- name: Cache PhantomJS
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.phantomjs.outputs.path }}
|
||||
key: ${{ runner.os }}-phantomjs
|
||||
restore-keys: ${{ runner.os }}-phantomjs
|
||||
key: ${{ matrix.config.os }}-phantomjs
|
||||
restore-keys: ${{ matrix.config.os }}-phantomjs
|
||||
- name: Install PhantomJS
|
||||
run: >
|
||||
Rscript
|
||||
@@ -116,83 +130,13 @@ jobs:
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{ runner.os }}-r${{ matrix.config.r }}-results
|
||||
name: ${{ matrix.config.os }}-r${{ matrix.config.r }}-results
|
||||
path: check
|
||||
|
||||
|
||||
documentation:
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
name: documentation
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config:
|
||||
- {os: macOS-latest, r: '4.0'}
|
||||
|
||||
env:
|
||||
R_REMOTES_NO_ERRORS_FROM_WARNINGS: true
|
||||
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: r-lib/actions/setup-r@master
|
||||
with:
|
||||
r-version: ${{ matrix.config.r }}
|
||||
|
||||
- name: Query dependencies
|
||||
run: |
|
||||
install.packages('remotes')
|
||||
saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2)
|
||||
shell: Rscript {0}
|
||||
- name: Cache R packages
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ env.R_LIBS_USER }}
|
||||
key: ${{ matrix.config.os }}-r-${{ matrix.config.r }}-2-${{ hashFiles('.github/depends.Rds') }}
|
||||
restore-keys: ${{ matrix.config.os }}-r-${{ matrix.config.r }}-2-
|
||||
- name: Remove dependencies file
|
||||
run: |
|
||||
rm .github/depends.Rds
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
install.packages(c("remotes"))
|
||||
remotes::install_deps(dependencies = TRUE)
|
||||
remotes::install_cran("devtools")
|
||||
remotes::install_cran("rprojroot")
|
||||
shell: Rscript {0}
|
||||
|
||||
- name: Check documentation
|
||||
run: |
|
||||
./tools/documentation/checkDocsCurrent.sh
|
||||
|
||||
|
||||
node_js:
|
||||
runs-on: macOS-latest
|
||||
name: node_js
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
# 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@v1
|
||||
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: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Check node build
|
||||
run: |
|
||||
./tools/checkJSCurrent.sh
|
||||
- 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
|
||||
|
||||
35
.github/workflows/pr-commands.yaml
vendored
35
.github/workflows/pr-commands.yaml
vendored
@@ -1,35 +0,0 @@
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
name: Commands
|
||||
jobs:
|
||||
document:
|
||||
if: startsWith(github.event.comment.body, '/document')
|
||||
name: document
|
||||
runs-on: macOS-latest
|
||||
env:
|
||||
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: r-lib/actions/pr-fetch@master
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: r-lib/actions/setup-r@master
|
||||
- name: Install dependencies
|
||||
run: Rscript -e 'install.packages(c("remotes", "roxygen2"))' -e 'remotes::install_deps(dependencies = TRUE)'
|
||||
- name: Document
|
||||
run: Rscript -e 'roxygen2::roxygenise()'
|
||||
- name: commit
|
||||
run: |
|
||||
git add man/\* NAMESPACE
|
||||
git commit -m 'Document'
|
||||
- uses: r-lib/actions/pr-push@master
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# added so that the workflow doesn't fail.
|
||||
always_runner:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Always run
|
||||
run: echo "This job is used to prevent the workflow status from showing as failed when all other jobs are skipped"
|
||||
153
.github/workflows/rituals.yaml
vendored
Normal file
153
.github/workflows/rituals.yaml
vendored
Normal 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
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,3 +10,6 @@ shinyapps/
|
||||
README.html
|
||||
.*.Rnb.cached
|
||||
tools/yarn-error.log
|
||||
|
||||
# GHA remotes installation
|
||||
.github/r-depends.rds
|
||||
|
||||
51
DESCRIPTION
51
DESCRIPTION
@@ -1,13 +1,18 @@
|
||||
Package: shiny
|
||||
Type: Package
|
||||
Title: Web Application Framework for R
|
||||
Version: 1.5.0.9001
|
||||
Version: 1.6.0.9000
|
||||
Authors@R: c(
|
||||
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
|
||||
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
|
||||
person("JJ", "Allaire", role = "aut", email = "jj@rstudio.com"),
|
||||
person("Carson", "Sievert", role = "aut", email = "carson@rstudio.com"),
|
||||
person("Barret", "Schloerke", role = "aut", email = "barret@rstudio.com"),
|
||||
person("Yihui", "Xie", role = "aut", email = "yihui@rstudio.com"),
|
||||
person("Jeff", "Allen", role = "aut", email = "jeff@rstudio.com"),
|
||||
person("Jonathan", "McPherson", role = "aut", email = "jonathan@rstudio.com"),
|
||||
person("Alan", "Dipert", role = "aut"),
|
||||
person("Barbara", "Borges", role = "aut"),
|
||||
person(family = "RStudio", role = "cph"),
|
||||
person(family = "jQuery Foundation", role = "cph",
|
||||
comment = "jQuery library and jQuery UI library"),
|
||||
@@ -75,53 +80,58 @@ Imports:
|
||||
mime (>= 0.3),
|
||||
jsonlite (>= 0.9.16),
|
||||
xtable,
|
||||
digest,
|
||||
htmltools (>= 0.4.0.9003),
|
||||
htmltools (>= 0.5.0.9001),
|
||||
R6 (>= 2.0),
|
||||
sourcetools,
|
||||
later (>= 1.0.0),
|
||||
promises (>= 1.1.0),
|
||||
tools,
|
||||
crayon,
|
||||
rlang (>= 0.4.0),
|
||||
fastmap (>= 1.0.0),
|
||||
rlang (>= 0.4.10),
|
||||
fastmap (>= 1.1.0),
|
||||
withr,
|
||||
commonmark (>= 1.7),
|
||||
glue (>= 1.3.2)
|
||||
glue (>= 1.3.2),
|
||||
bslib (>= 0.2.2.9002),
|
||||
cachem,
|
||||
ellipsis,
|
||||
lifecycle (>= 0.2.0)
|
||||
Suggests:
|
||||
datasets,
|
||||
Cairo (>= 1.5-5),
|
||||
testthat (>= 2.1.1),
|
||||
testthat (>= 3.0.0),
|
||||
knitr (>= 1.6),
|
||||
markdown,
|
||||
rmarkdown,
|
||||
ggplot2,
|
||||
reactlog (>= 1.0.0),
|
||||
magrittr,
|
||||
shinytest,
|
||||
shinytest (>= 1.4.0.9003),
|
||||
yaml,
|
||||
future,
|
||||
dygraphs,
|
||||
ragg,
|
||||
showtext
|
||||
URL: http://shiny.rstudio.com
|
||||
showtext,
|
||||
sass
|
||||
URL: https://shiny.rstudio.com/
|
||||
BugReports: https://github.com/rstudio/shiny/issues
|
||||
Collate:
|
||||
'app.R'
|
||||
'globals.R'
|
||||
'app-state.R'
|
||||
'app_template.R'
|
||||
'bind-cache.R'
|
||||
'bind-event.R'
|
||||
'bookmark-state-local.R'
|
||||
'stack.R'
|
||||
'bookmark-state.R'
|
||||
'bootstrap-deprecated.R'
|
||||
'bootstrap-layout.R'
|
||||
'globals.R'
|
||||
'conditions.R'
|
||||
'map.R'
|
||||
'utils.R'
|
||||
'bootstrap.R'
|
||||
'cache-disk.R'
|
||||
'cache-memory.R'
|
||||
'cache-utils.R'
|
||||
'deprecated.R'
|
||||
'devmode.R'
|
||||
'diagnose.R'
|
||||
'fileupload.R'
|
||||
'font-awesome.R'
|
||||
@@ -131,7 +141,6 @@ Collate:
|
||||
'history.R'
|
||||
'hooks.R'
|
||||
'html-deps.R'
|
||||
'htmltools.R'
|
||||
'image-interact-opts.R'
|
||||
'image-interact.R'
|
||||
'imageutils.R'
|
||||
@@ -153,6 +162,7 @@ Collate:
|
||||
'insert-tab.R'
|
||||
'insert-ui.R'
|
||||
'jqueryui.R'
|
||||
'knitr.R'
|
||||
'middleware-shiny.R'
|
||||
'middleware.R'
|
||||
'timer.R'
|
||||
@@ -169,10 +179,14 @@ Collate:
|
||||
'render-plot.R'
|
||||
'render-table.R'
|
||||
'run-url.R'
|
||||
'runapp.R'
|
||||
'serializers.R'
|
||||
'server-input-handlers.R'
|
||||
'server-resource-paths.R'
|
||||
'server.R'
|
||||
'shiny-options.R'
|
||||
'shiny-package.R'
|
||||
'shinyapp.R'
|
||||
'shinyui.R'
|
||||
'shinywrappers.R'
|
||||
'showcase.R'
|
||||
@@ -182,6 +196,11 @@ Collate:
|
||||
'test-server.R'
|
||||
'test.R'
|
||||
'update-input.R'
|
||||
'utils-lang.R'
|
||||
'version_jquery.R'
|
||||
'viewer.R'
|
||||
RoxygenNote: 7.1.1
|
||||
Encoding: UTF-8
|
||||
Roxygen: list(markdown = TRUE)
|
||||
RdMacros: lifecycle
|
||||
Config/testthat/edition: 3
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -335,7 +335,7 @@ THE SOFTWARE.
|
||||
|
||||
----
|
||||
|
||||
Copyright (c) 2014, Dave Gandy http://fontawesome.io/,
|
||||
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.
|
||||
|
||||
58
NAMESPACE
58
NAMESPACE
@@ -25,6 +25,22 @@ S3method(as.shiny.appobj,list)
|
||||
S3method(as.shiny.appobj,shiny.appobj)
|
||||
S3method(as.tags,shiny.appobj)
|
||||
S3method(as.tags,shiny.render.function)
|
||||
S3method(bindCache,"function")
|
||||
S3method(bindCache,Observer)
|
||||
S3method(bindCache,default)
|
||||
S3method(bindCache,reactive.cache)
|
||||
S3method(bindCache,reactive.event)
|
||||
S3method(bindCache,reactiveExpr)
|
||||
S3method(bindCache,shiny.render.function)
|
||||
S3method(bindCache,shiny.render.function.cache)
|
||||
S3method(bindCache,shiny.render.function.event)
|
||||
S3method(bindCache,shiny.renderPlot)
|
||||
S3method(bindEvent,Observer)
|
||||
S3method(bindEvent,Observer.event)
|
||||
S3method(bindEvent,default)
|
||||
S3method(bindEvent,reactive.event)
|
||||
S3method(bindEvent,reactiveExpr)
|
||||
S3method(bindEvent,shiny.render.function)
|
||||
S3method(format,reactiveExpr)
|
||||
S3method(format,reactiveVal)
|
||||
S3method(names,reactivevalues)
|
||||
@@ -50,6 +66,8 @@ export(animationOptions)
|
||||
export(appendTab)
|
||||
export(as.shiny.appobj)
|
||||
export(basicPage)
|
||||
export(bindCache)
|
||||
export(bindEvent)
|
||||
export(bookmarkButton)
|
||||
export(bootstrapLib)
|
||||
export(bootstrapPage)
|
||||
@@ -73,6 +91,7 @@ export(dateInput)
|
||||
export(dateRangeInput)
|
||||
export(dblclickOpts)
|
||||
export(debounce)
|
||||
export(devmode)
|
||||
export(dialogViewer)
|
||||
export(diskCache)
|
||||
export(div)
|
||||
@@ -99,10 +118,12 @@ export(formatStackTrace)
|
||||
export(freezeReactiveVal)
|
||||
export(freezeReactiveValue)
|
||||
export(getCurrentOutputInfo)
|
||||
export(getCurrentTheme)
|
||||
export(getDefaultReactiveDomain)
|
||||
export(getQueryString)
|
||||
export(getShinyOption)
|
||||
export(getUrlHash)
|
||||
export(get_devmode_option)
|
||||
export(h1)
|
||||
export(h2)
|
||||
export(h3)
|
||||
@@ -120,6 +141,7 @@ export(httpResponse)
|
||||
export(icon)
|
||||
export(imageOutput)
|
||||
export(img)
|
||||
export(in_devmode)
|
||||
export(incProgress)
|
||||
export(includeCSS)
|
||||
export(includeHTML)
|
||||
@@ -180,8 +202,10 @@ export(pre)
|
||||
export(prependTab)
|
||||
export(printError)
|
||||
export(printStackTrace)
|
||||
export(quoToFunction)
|
||||
export(radioButtons)
|
||||
export(reactive)
|
||||
export(reactiveConsole)
|
||||
export(reactiveFileReader)
|
||||
export(reactivePlot)
|
||||
export(reactivePoll)
|
||||
@@ -197,6 +221,7 @@ export(reactlog)
|
||||
export(reactlogReset)
|
||||
export(reactlogShow)
|
||||
export(registerInputHandler)
|
||||
export(registerThemeDependency)
|
||||
export(removeInputHandler)
|
||||
export(removeModal)
|
||||
export(removeNotification)
|
||||
@@ -307,13 +332,15 @@ export(withMathJax)
|
||||
export(withProgress)
|
||||
export(withReactiveDomain)
|
||||
export(withTags)
|
||||
export(with_devmode)
|
||||
import(R6)
|
||||
import(digest)
|
||||
import(htmltools)
|
||||
import(httpuv)
|
||||
import(methods)
|
||||
import(mime)
|
||||
import(xtable)
|
||||
importFrom(ellipsis,check_dots_empty)
|
||||
importFrom(ellipsis,check_dots_unnamed)
|
||||
importFrom(fastmap,fastmap)
|
||||
importFrom(fastmap,is.key_missing)
|
||||
importFrom(fastmap,key_missing)
|
||||
@@ -357,5 +384,34 @@ importFrom(htmltools,tagSetChildren)
|
||||
importFrom(htmltools,tags)
|
||||
importFrom(htmltools,validateCssUnit)
|
||||
importFrom(htmltools,withTags)
|
||||
importFrom(lifecycle,deprecated)
|
||||
importFrom(promises,"%...!%")
|
||||
importFrom(promises,"%...>%")
|
||||
importFrom(promises,as.promise)
|
||||
importFrom(promises,is.promising)
|
||||
importFrom(promises,promise)
|
||||
importFrom(promises,promise_reject)
|
||||
importFrom(promises,promise_resolve)
|
||||
importFrom(rlang,"%||%")
|
||||
importFrom(rlang,as_function)
|
||||
importFrom(rlang,as_quosure)
|
||||
importFrom(rlang,enexpr)
|
||||
importFrom(rlang,enquo)
|
||||
importFrom(rlang,enquos)
|
||||
importFrom(rlang,enquos0)
|
||||
importFrom(rlang,eval_tidy)
|
||||
importFrom(rlang,expr)
|
||||
importFrom(rlang,get_env)
|
||||
importFrom(rlang,get_expr)
|
||||
importFrom(rlang,inject)
|
||||
importFrom(rlang,is_false)
|
||||
importFrom(rlang,is_missing)
|
||||
importFrom(rlang,is_na)
|
||||
importFrom(rlang,is_quosure)
|
||||
importFrom(rlang,maybe_missing)
|
||||
importFrom(rlang,missing_arg)
|
||||
importFrom(rlang,new_function)
|
||||
importFrom(rlang,new_quosure)
|
||||
importFrom(rlang,pairlist2)
|
||||
importFrom(rlang,quo)
|
||||
importFrom(rlang,zap_srcref)
|
||||
|
||||
121
NEWS.md
121
NEWS.md
@@ -1,13 +1,61 @@
|
||||
|
||||
shiny 1.5.0.9000
|
||||
shiny 1.6.0.9000
|
||||
================
|
||||
|
||||
## Full changelog
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The `format` and `locale` arguments to `sliderInput()` have been removed. They have been deprecated since 0.10.2.2 (released on 2014-12-08).
|
||||
|
||||
### Minor 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)
|
||||
|
||||
* Switched from `digest::digest()` to `rlang::hash()` for hashing. (#3264)
|
||||
|
||||
* Switched from internal `Stack` class to `fastmap::faststack()`, and used `fastmap::fastqueue()`. (#3176)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
### Library updates
|
||||
|
||||
* Closed #3286: Updated to Font-Awesome 5.15.2. (#3288)
|
||||
|
||||
* Updated to jQuery 3.6.0. (#3311)
|
||||
|
||||
shiny 1.6.0
|
||||
===========
|
||||
|
||||
This release focuses on improvements in three main areas:
|
||||
|
||||
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 `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/)
|
||||
|
||||
2. Caching of `reactive()` and `render*()` (e.g. `renderText()`, `renderTable()`, etc) expressions.
|
||||
* Such expressions automatically cache their _most recent value_, which helps to avoid redundant computation within a single "flush" of reactivity. The new `bindCache()` function can be used to cache _all previous values_ (as long as they fit in the cache). This cache may be optionally scoped within and/or across user sessions, possibly leading to huge performance gains, especially when deployed at scale across user sessions.
|
||||
* For more details, see `help(bindCache, package = "shiny")`
|
||||
|
||||
3. Various improvements to accessibility for screen-reader and keyboard users.
|
||||
* For more details, see the accessibility section below.
|
||||
|
||||
## Full changelog
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Closed #3074: Shiny no longer supports file uploads for Internet Explorer 8 or 9. (#3075)
|
||||
|
||||
* Subtle changes, and some soft-deprecations, have come to `freezeReactiveValue` and `freezeReactiveVal` (#3055). These functions have been fragile at best in previous releases (issues #1791, #2463, #2946). In this release, we've solved all the problems we know about with `freezeReactiveValue(input, "x")`, by 1) invalidating `input$x` and set it to `NULL` whenever we freeze, and 2) ensuring that, after a freeze, even if the effect of `renderUI` or `updateXXXInput` is to set `input$x` to the same value it already has, this will result in an invalidation (whereas by default, Shiny filters out such spurious assignments).
|
||||
|
||||
Similar problems may exist when using `freezeReactiveVal`, and when using `freezeReactiveValue` with non-`input` reactive values objects. But support for those was added mostly for symmetry with `freezeReactiveValue(input)`, and given the above issues, it's not clear to us how you could have used these successfully in the past, or why you would even want to. For this release, we're soft-deprecating both of those uses, but we're more than willing to un-deprecate if it turns out people are using these; if that includes you, please join the conversation at https://github.com/rstudio/shiny/issues/3063. In the meantime, you can squelch the deprecation messages for these functions specifically, by setting `options(shiny.deprecation.messages.freeze = FALSE)`.
|
||||
|
||||
### Accessibility
|
||||
|
||||
* Added [bootstrap accessibility plugin](https://github.com/paypal/bootstrap-accessibility-plugin) under the hood to improve accessibility of shiny apps for screen-reader and keyboard users: the enhancements include better navigations for alert, tooltip, popover, modal dialog, dropdown, tab Panel, collapse, and carousel elements. (#2911)
|
||||
|
||||
* Closed #2987: Improved accessibility of "live regions" -- namely, `*Output()` bindings and `update*Input()`. (#3042)
|
||||
|
||||
* Added appropriate labels to `icon()` element to provide screen-reader users with alternative descriptions for the `fontawesome` and `glyphicon`: `aria-label` is automatically applied based on the fontawesome name. For example, `icon("calendar")` will be announced as "calendar icon" to screen readers. "presentation" aria role has also been attached to `icon()` to remove redundant semantic info for screen readers. (#2917)
|
||||
|
||||
* Closed #2929: Fixed keyboard accessibility for file picker button: keyboard users can now tab to focus on `fileInput()` widget. (#2937)
|
||||
@@ -18,10 +66,22 @@ shiny 1.5.0.9000
|
||||
|
||||
* 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)
|
||||
|
||||
* Added semantic landmarks for `mainPanel()` and `sidebarPanel()` so that assistive technologies can recognize them as "main" and "complementary" region respectively. (#3009)
|
||||
|
||||
* Closed #2844: Added `lang` argument to ui `*Page()` functions (e.g., `fluidPage`, `bootstrapPage`) that specifies document-level language within the app for the accessibility of screen readers and search-engine parsers. By default, it is set to empty string which is commonly recognized as a browser's default locale. (#2920)
|
||||
|
||||
* Improved accessibility for `radioButtons()` and `checkboxGroupInput()`: All options are now grouped together semantically for assistive technologies. (thanks @jooyoungseo, #3187).
|
||||
|
||||
### Minor new features and improvements
|
||||
|
||||
* Added support for Shiny Developer Mode. Developer Mode enables a number of `options()` to make a developer's life easier, like enabling non-minified JS and printing messages about deprecated functions and options. See `?devmode()` for more details. (#3174)
|
||||
|
||||
* New `reactiveConsole()` makes it easier to interactively experiment with reactivity at the console (#2518).
|
||||
|
||||
* When UI is specified as a function (e.g. `ui <- function(req) { ... }`), the response can now be an HTTP response as returned from the (newly exported) `httpResponse()` function. (#2970)
|
||||
|
||||
* `selectInput` and `selectizeInput` now warn about performance implications when thousands of choices are used, and recommend [server-side selectize](https://shiny.rstudio.com/articles/selectize.html) be used instead. (#2959)
|
||||
|
||||
* Closed #2980: `addResourcePath()` now allows paths with a leading `.` (thanks to @ColinFay). (#2981)
|
||||
|
||||
* Closed #2972: `runExample()` now supports the `shiny.port` option (thanks to @ColinFay). (#2982)
|
||||
@@ -34,14 +94,38 @@ shiny 1.5.0.9000
|
||||
|
||||
* Removed unused (and non-exported) `cacheContext` class.
|
||||
|
||||
### Bug fixes
|
||||
* `testServer()` can accept a single server function as input (#2965).
|
||||
|
||||
* Fixed #2859: `renderPlot()` wasn't correctly setting `showtext::showtext_opts()`'s `dpi` setting with the correct resolution on high resolution displays; which means, if the font was rendered by showtext, font sizes would look smaller than they should on such displays. (#2941)
|
||||
* `shinyOptions()` now has session-level scoping, in addition to global and application-level scoping. (#3080)
|
||||
|
||||
* `runApp()` now warns when running an application in an R package directory. (#3114)
|
||||
|
||||
* Shiny now uses `cache_mem` from the cachem package, instead of `memoryCache` and `diskCache`. (#3118)
|
||||
|
||||
* Closed #3140: Added support for `...` argument in `icon()`. (#3143)
|
||||
|
||||
* Closed #629: All `update*` functions now have a default value for `session`, and issue an informative warning if it is missing. (#3195, #3199)
|
||||
|
||||
* Improved error messages when reading reactive values outside of a reactive domain (e.g., `reactiveVal()()`). (#3007)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fixed #1942: Calling `runApp("app.R")` no longer ignores options passed into `shinyApp()`. This makes it possible for Shiny apps to specify what port/host should be used by default. (#2969)
|
||||
|
||||
* Fixed #3033: When a `DiskCache` was created with both `max_n` and `max_size`, too many items could get pruned when `prune()` was called. (#3034)
|
||||
|
||||
* Fixed #2703: Fixed numerous issues with some combinations of `min`/`value`/`max` causing issues with `date[Range]Input()` and `updateDate[Range]Input()`. (#3038, #3201)
|
||||
|
||||
* Fixed #2936: `dateYMD` was giving a warning when passed a vector of dates from `dateInput` which was greater than length 1. The length check was removed because it was not needed. (#3061)
|
||||
|
||||
* Fixed #2266, #2688: `radioButtons` and `updateRadioButtons` now accept `character(0)` to indicate that none of the options should be selected (thanks to @ColinFay). (#3043)
|
||||
|
||||
* Fixed a bug that `textAreaInput()` doesn't work as expected for relative `width` (thanks to @shrektan). (#2049)
|
||||
|
||||
* Fixed #2859: `renderPlot()` wasn't correctly setting `showtext::showtext_opts()`'s `dpi` setting with the correct resolution on high resolution displays; which means, if the font was rendered by showtext, font sizes would look smaller than they should on such displays. (#2941)
|
||||
|
||||
* Closed #2910, #2909, #1552: `sliderInput()` warns if the `value` is outside of `min` and `max`, and errors if `value` is `NULL` or `NA`. (#3194)
|
||||
|
||||
### Library updates
|
||||
|
||||
* Removed html5shiv and respond.js, which were used for IE 8 and IE 9 compatibility. (#2973)
|
||||
@@ -234,7 +318,7 @@ shiny 1.3.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](http://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.
|
||||
|
||||
## Full changelog
|
||||
|
||||
@@ -360,6 +444,7 @@ This is a significant release for Shiny, with a major new feature that was nearl
|
||||
|
||||
* Fixed #2021: Memory leak with `reactiveTimer` and `invalidateLater`. (#2022)
|
||||
|
||||
|
||||
### Library updates
|
||||
|
||||
* Updated to ion.rangeSlider 2.2.0. (#1955)
|
||||
@@ -657,7 +742,7 @@ This is a maintenance release of Shiny, with some bug fixes and minor new featur
|
||||
|
||||
* Restored file inputs are now copied on restore, so that the restored application can't modify the bookmarked file. (#1370)
|
||||
|
||||
* Added support for plot interaction in the development version of ggplot2, 2.1.0.9000. Also added support for ggplot2 plots with `coord_flip()` (in the development version of ggplot2). ([hadley/ggplot2#1781](https://github.com/hadley/ggplot2/issues/1781), #1392)
|
||||
* Added support for plot interaction in the development version of ggplot2, 2.1.0.9000. Also added support for ggplot2 plots with `coord_flip()` (in the development version of ggplot2). ([hadley/ggplot2#1781](https://github.com/tidyverse/ggplot2/issues/1781), #1392)
|
||||
|
||||
|
||||
### Bug fixes
|
||||
@@ -689,7 +774,7 @@ Shiny now supports bookmarkable state: users can save the state of an applicatio
|
||||
**_Important note_:**
|
||||
> Saved-to-server bookmarking currently works with Shiny Server Open Source. Support on Shiny Server Pro, RStudio Connect, and shinyapps.io is under development and testing. However, URL-encoded bookmarking works on all hosting platforms.
|
||||
|
||||
See [this article](http://shiny.rstudio.com/articles/bookmarking-state.html) to get started with bookmarkable state. There is also an [advanced-level article](http://shiny.rstudio.com/articles/advanced-bookmarking.html) (for apps that have a complex state), and [a modules article](http://shiny.rstudio.com/articles/bookmarking-modules.html) that details how to use bookmarking in conjunction with modules.
|
||||
See [this article](https://shiny.rstudio.com/articles/bookmarking-state.html) to get started with bookmarkable state. There is also an [advanced-level article](https://shiny.rstudio.com/articles/advanced-bookmarking.html) (for apps that have a complex state), and [a modules article](https://shiny.rstudio.com/articles/bookmarking-modules.html) that details how to use bookmarking in conjunction with modules.
|
||||
|
||||
## Notifications
|
||||
|
||||
@@ -699,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%"/>
|
||||
</p>
|
||||
|
||||
[Here](http://shiny.rstudio.com/articles/notifications.html)'s our article about it, and the [reference documentation](http://shiny.rstudio.com/reference/shiny/latest/showNotification.html).
|
||||
[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
|
||||
|
||||
@@ -708,7 +793,7 @@ If your Shiny app contains computations that take a long time to complete, a pro
|
||||
**_Important note_:**
|
||||
> If you were already using progress bars and had customized them with your own CSS, you can add the `style = "old"` argument to your `withProgress()` call (or `Progress$new()`). This will result in the same appearance as before. You can also call `shinyOptions(progress.style = "old")` in your app's server function to make all progress indicators use the old styling.
|
||||
|
||||
To see new progress bars in action, see [this app](https://gallery.shinyapps.io/085-progress/) in the gallery. You can also learn more about this in [our article](http://shiny.rstudio.com/articles/progress.html) and in the reference documentation (either for the easier [`withProgress` functional API](http://shiny.rstudio.com/reference/shiny/latest/withProgress.html) or the more complicated, but more powerful, [`Progress` object-oriented API](http://shiny.rstudio.com/reference/shiny/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
|
||||
|
||||
@@ -722,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%"/>
|
||||
</p>
|
||||
|
||||
To learn more about this, read [our article](http://shiny.rstudio.com/articles/modal-dialogs.html) and the [reference documentation](http://shiny.rstudio.com/reference/shiny/latest/modalDialog.html).
|
||||
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`
|
||||
|
||||
@@ -730,13 +815,13 @@ 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`.
|
||||
|
||||
For more, read [our article](http://shiny.rstudio.com/articles/dynamic-ui.html) about dynamic UI generation and the reference documentation about [`insertUI`](http://shiny.rstudio.com/reference/shiny/latest/insertUI.html) and [`removeUI`](http://shiny.rstudio.com/reference/shiny/latest/removeUI.html).
|
||||
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
|
||||
|
||||
Many Shiny users have asked about best practices for accessing external databases from their Shiny applications. Although database access has long been possible using various database connector packages in R, it can be challenging to use them robustly in the dynamic environment that Shiny provides. So far, it has been mostly up to application authors to find the appropriate database drivers and to discover how to manage the database connections within an application. In order to demystify this process, we wrote a series of articles ([first one here](http://shiny.rstudio.com/articles/overview.html)) that covers the basics of connecting to an external database, as well as some security precautions to keep in mind (e.g. [how to avoid SQL injection attacks](http://shiny.rstudio.com/articles/sql-injections.html)).
|
||||
Many Shiny users have asked about best practices for accessing external databases from their Shiny applications. Although database access has long been possible using various database connector packages in R, it can be challenging to use them robustly in the dynamic environment that Shiny provides. So far, it has been mostly up to application authors to find the appropriate database drivers and to discover how to manage the database connections within an application. In order to demystify this process, we wrote a series of articles ([first one here](https://shiny.rstudio.com/articles/overview.html)) that covers the basics of connecting to an external database, as well as some security precautions to keep in mind (e.g. [how to avoid SQL injection attacks](https://shiny.rstudio.com/articles/sql-injections.html)).
|
||||
|
||||
There are a few packages that you should look at if you're using a relational database in a Shiny app: the `dplyr` and `DBI` packages (both featured in the article linked to above), and the brand new `pool` package, which provides a further layer of abstraction to make it easier and safer to use either `DBI` or `dplyr`. `pool` is not yet on CRAN. In particular, `pool` will take care of managing connections, preventing memory leaks, and ensuring the best performance. See this [`pool` basics article](http://shiny.rstudio.com/articles/pool-basics.html) and the [more advanced-level article](http://shiny.rstudio.com/articles/pool-advanced.html) if you're feeling adventurous! (Both of these articles contain Shiny app examples that use `DBI` to connect to an external MySQL database.) If you are more comfortable with `dplyr` than `DBI`, don't miss the article about the [integration of `pool` and `dplyr`](http://shiny.rstudio.com/articles/pool-dplyr.html).
|
||||
There are a few packages that you should look at if you're using a relational database in a Shiny app: the `dplyr` and `DBI` packages (both featured in the article linked to above), and the brand new `pool` package, which provides a further layer of abstraction to make it easier and safer to use either `DBI` or `dplyr`. `pool` is not yet on CRAN. In particular, `pool` will take care of managing connections, preventing memory leaks, and ensuring the best performance. See this [`pool` basics article](https://shiny.rstudio.com/articles/pool-basics.html) and the [more advanced-level article](https://shiny.rstudio.com/articles/pool-advanced.html) if you're feeling adventurous! (Both of these articles contain Shiny app examples that use `DBI` to connect to an external MySQL database.) If you are more comfortable with `dplyr` than `DBI`, don't miss the article about the [integration of `pool` and `dplyr`](https://shiny.rstudio.com/articles/pool-dplyr.html).
|
||||
|
||||
If you're new to databases in the Shiny world, we recommend using `dplyr` and `pool` if possible. If you need greater control than `dplyr` offers (for example, if you need to modify data in the database or use transactions), then use `DBI` and `pool`. The `pool` package was introduced to make your life easier, but in no way constrains you, so we don't envision any situation in which you'd be better off *not* using it. The only caveat is that `pool` is not yet on CRAN, so you may prefer to wait for that.
|
||||
|
||||
@@ -744,11 +829,11 @@ If you're new to databases in the Shiny world, we recommend using `dplyr` and `p
|
||||
|
||||
There are many more minor features, small improvements, and bug fixes than we can cover here, so we'll just mention a few of the more noteworthy ones (the full changelog, with links to all the relevant issues and pull requests, is right below this section):
|
||||
|
||||
* **Error Sanitization**: you now have the option to sanitize error messages; in other words, the content of the original error message can be suppressed so that it doesn't leak any sensitive information. To sanitize errors everywhere in your app, just add `options(shiny.sanitize.errors = TRUE)` somewhere in your app. Read [this article](http://shiny.rstudio.com/articles/sanitize-errors.html) for more, or play with the [demo app](https://gallery.shinyapps.io/110-error-sanitization/).
|
||||
* **Error Sanitization**: you now have the option to sanitize error messages; in other words, the content of the original error message can be suppressed so that it doesn't leak any sensitive information. To sanitize errors everywhere in your app, just add `options(shiny.sanitize.errors = TRUE)` somewhere in your app. Read [this article](https://shiny.rstudio.com/articles/sanitize-errors.html) for more, or play with the [demo app](https://gallery.shinyapps.io/110-error-sanitization/).
|
||||
|
||||
* **Code Diagnostics**: if there is an error parsing `ui.R`, `server.R`, `app.R`, or `global.R`, Shiny will search the code for missing commas, extra commas, and unmatched braces, parens, and brackets, and will print out messages pointing out those problems. (#1126)
|
||||
|
||||
* **Reactlog visualization**: by default, the [`showReactLog()` function](http://shiny.rstudio.com/reference/shiny/latest/reactlog.html) (which brings up the reactive graph) also displays the time that each reactive and observer were active for:
|
||||
* **Reactlog visualization**: by default, the [`showReactLog()` function](https://shiny.rstudio.com/reference/shiny/latest/reactlog.html) (which brings up the reactive graph) also displays the time that each reactive and observer were active for:
|
||||
|
||||
<p align="center">
|
||||
<img src="http://shiny.rstudio.com/images/reactlog.png" alt="modal-dialog" width="75%"/>
|
||||
@@ -764,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%"/>
|
||||
</p>
|
||||
|
||||
For more, read our [short article](http://shiny.rstudio.com/articles/render-table.html) about this update, experiment with all the new features in this [demo app](https://gallery.shinyapps.io/109-render-table/), or check out the [reference documentation](http://shiny.rstudio.com/reference/shiny/latest/renderTable.html).
|
||||
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
|
||||
|
||||
@@ -1091,7 +1176,7 @@ Shiny 0.11 switches away from the Bootstrap 2 web framework to the next version,
|
||||
* `updateSliderInput()` can now control the min, max, value, and step size of a slider. Previously, only the value could be controlled this way, and if you wanted to change other values, you needed to use Shiny's dynamic UI.
|
||||
|
||||
|
||||
* If in your HTML you are using custom CSS classes that are specific to Bootstrap, you may need to update them for Bootstrap 3. See the Bootstrap [migration guide](http://getbootstrap.com/migration/).
|
||||
* If in your HTML you are using custom CSS classes that are specific to Bootstrap, you may need to update them for Bootstrap 3. See the Bootstrap [migration guide](https://getbootstrap.com/migration/).
|
||||
|
||||
|
||||
If you encounter other migration issues, please let us know on the [shiny-discuss](https://groups.google.com/forum/#!forum/shiny-discuss) mailing list, or on the Shiny [issue tracker](https://github.com/rstudio/shiny/issues).
|
||||
@@ -1284,7 +1369,7 @@ shiny 0.9.0
|
||||
|
||||
* Added `theme` parameter to page building functions for specifying alternate bootstrap css styles.
|
||||
|
||||
* Added `icon()` function for embedding icons from the [font awesome](http://fontawesome.io/) icon library
|
||||
* Added `icon()` function for embedding icons from the [font awesome](https://fontawesome.com) icon library
|
||||
|
||||
* Added `makeReactiveBinding` function to turn a "regular" variable into a reactive one (i.e. reading the variable makes the current reactive context dependent on it, and setting the variable is a source of reactivity).
|
||||
|
||||
|
||||
26
R/app-state.R
Normal file
26
R/app-state.R
Normal file
@@ -0,0 +1,26 @@
|
||||
#' @include globals.R
|
||||
NULL
|
||||
|
||||
# The current app state is a place to read and hang state for the
|
||||
# currently-running application. This is useful for setting options that will
|
||||
# last as long as the application is running.
|
||||
|
||||
.globals$appState <- NULL
|
||||
|
||||
initCurrentAppState <- function(appobj) {
|
||||
if (!is.null(.globals$appState)) {
|
||||
stop("Can't initialize current app state when another is currently active.")
|
||||
}
|
||||
.globals$appState <- new.env(parent = emptyenv())
|
||||
.globals$appState$app <- appobj
|
||||
# Copy over global options
|
||||
.globals$appState$options <- .globals$options
|
||||
}
|
||||
|
||||
getCurrentAppState <- function() {
|
||||
.globals$appState
|
||||
}
|
||||
|
||||
clearCurrentAppState <- function() {
|
||||
.globals$appState <- NULL
|
||||
}
|
||||
774
R/bind-cache.R
Normal file
774
R/bind-cache.R
Normal file
@@ -0,0 +1,774 @@
|
||||
utils::globalVariables(".GenericCallEnv", add = TRUE)
|
||||
|
||||
#' Add caching with reactivity to an object
|
||||
#'
|
||||
#' @description
|
||||
#'
|
||||
#' `bindCache()` adds caching [reactive()] expressions and `render*` functions
|
||||
#' (like [renderText()], [renderTable()], ...).
|
||||
#'
|
||||
#' Ordinary [reactive()] expressions automatically cache their _most recent_
|
||||
#' value, which helps to avoid redundant computation in downstream reactives.
|
||||
#' `bindCache()` will cache all previous values (as long as they fit in the
|
||||
#' cache) and they can be shared across user sessions. This allows
|
||||
#' `bindCache()` to dramatically improve performance when used correctly.
|
||||
|
||||
#' @details
|
||||
#'
|
||||
#' `bindCache()` requires one or more expressions that are used to generate a
|
||||
#' **cache key**, which is used to determine if a computation has occurred
|
||||
#' before and hence can be retrieved from the cache. If you're familiar with the
|
||||
#' concept of memoizing pure functions (e.g., the \pkg{memoise} package), you
|
||||
#' can think of the cache key as the input(s) to a pure function. As such, one
|
||||
#' should take care to make sure the use of `bindCache()` is _pure_ in the same
|
||||
#' sense, namely:
|
||||
#'
|
||||
#' 1. For a given key, the return value is always the same.
|
||||
#' 2. Evaluation has no side-effects.
|
||||
#'
|
||||
#' In the example here, the `bindCache()` key consists of `input$x` and
|
||||
#' `input$y` combined, and the value is `input$x * input$y`. In this simple
|
||||
#' example, for any given key, there is only one possible returned value.
|
||||
#'
|
||||
#' ```
|
||||
#' r <- reactive({ input$x * input$y }) %>%
|
||||
#' bindCache(input$x, input$y)
|
||||
#' ```
|
||||
#'
|
||||
|
||||
#' The largest performance improvements occur when the cache key is fast to
|
||||
#' compute and the reactive expression is slow to compute. To see if the value
|
||||
#' should be computed, a cached reactive evaluates the key, and then serializes
|
||||
#' and hashes the result. If the resulting hashed key is in the cache, then the
|
||||
#' cached reactive simply retrieves the previously calculated value and returns
|
||||
#' it; if not, then the value is computed and the result is stored in the cache
|
||||
#' before being returned.
|
||||
#'
|
||||
#' To compute the cache key, `bindCache()` hashes the contents of `...`, so it's
|
||||
#' best to avoid including large objects in a cache key since that can result in
|
||||
#' slow hashing. It's also best to avoid reference objects like environments and
|
||||
#' R6 objects, since the serialization of these objects may not capture relevant
|
||||
#' changes.
|
||||
#'
|
||||
#' If you want to use a large object as part of a cache key, it may make sense
|
||||
#' to do some sort of reduction on the data that still captures information
|
||||
#' about whether a value can be retrieved from the cache. For example, if you
|
||||
#' have a large data set with timestamps, it might make sense to extract the
|
||||
#' most recent timestamp and return that. Then, instead of hashing the entire
|
||||
#' data object, the cached reactive only needs to hash the timestamp.
|
||||
#'
|
||||
#' ```
|
||||
#' r <- reactive({ compute(bigdata()) } %>%
|
||||
#' bindCache({ extract_most_recent_time(bigdata()) })
|
||||
#' ```
|
||||
#'
|
||||
#' For computations that are very slow, it often makes sense to pair
|
||||
#' [bindCache()] with [bindEvent()] so that no computation is performed until
|
||||
#' the user explicitly requests it (for more, see the Details section of
|
||||
#' [bindEvent()]).
|
||||
|
||||
#' @section Cache keys and reactivity:
|
||||
#'
|
||||
#' Because the **value** expression (from the original [reactive()]) is
|
||||
#' cached, it is not necessarily re-executed when someone retrieves a value,
|
||||
#' and therefore it can't be used to decide what objects to take reactive
|
||||
#' dependencies on. Instead, the **key** is used to figure out which objects
|
||||
#' to take reactive dependencies on. In short, the key expression is reactive,
|
||||
#' and value expression is no longer reactive.
|
||||
#'
|
||||
#' Here's an example of what not to do: if the key is `input$x` and the value
|
||||
#' expression is from `reactive({input$x + input$y})`, then the resulting
|
||||
#' cached reactive will only take a reactive dependency on `input$x` -- it
|
||||
#' won't recompute `{input$x + input$y}` when just `input$y` changes.
|
||||
#' Moreover, the cache won't use `input$y` as part of the key, and so it could
|
||||
#' return incorrect values in the future when it retrieves values from the
|
||||
#' cache. (See the examples below for an example of this.)
|
||||
#'
|
||||
#' A better cache key would be something like `input$x, input$y`. This does
|
||||
#' two things: it ensures that a reactive dependency is taken on both
|
||||
#' `input$x` and `input$y`, and it also makes sure that both values are
|
||||
#' represented in the cache key.
|
||||
#'
|
||||
#' In general, `key` should use the same reactive inputs as `value`, but the
|
||||
#' computation should be simpler. If there are other (non-reactive) values
|
||||
#' that are consumed, such as external data sources, they should be used in
|
||||
#' the `key` as well. Note that if the `key` is large, it can make sense to do
|
||||
#' some sort of reduction on it so that the serialization and hashing of the
|
||||
#' cache key is not too expensive.
|
||||
#'
|
||||
#' Remember that the key is _reactive_, so it is not re-executed every single
|
||||
#' time that someone accesses the cached reactive. It is only re-executed if
|
||||
#' it has been invalidated by one of the reactives it depends on. For
|
||||
#' example, suppose we have this cached reactive:
|
||||
#'
|
||||
#' ```
|
||||
#' r <- reactive({ input$x * input$y }) %>%
|
||||
#' bindCache(input$x, input$y)
|
||||
#' ```
|
||||
#'
|
||||
#' In this case, the key expression is essentially `reactive(list(input$x,
|
||||
#' input$y))` (there's a bit more to it, but that's a good enough
|
||||
#' approximation). The first time `r()` is called, it executes the key, then
|
||||
#' fails to find it in the cache, so it executes the value expression, `{
|
||||
#' input$x + input$y }`. If `r()` is called again, then it does not need to
|
||||
#' re-execute the key expression, because it has not been invalidated via a
|
||||
#' change to `input$x` or `input$y`; it simply returns the previous value.
|
||||
#' However, if `input$x` or `input$y` changes, then the reactive expression will
|
||||
#' be invalidated, and the next time that someone calls `r()`, the key
|
||||
#' expression will need to be re-executed.
|
||||
#'
|
||||
#' Note that if the cached reactive is passed to [bindEvent()], then the key
|
||||
#' expression will no longer be reactive; instead, the event expression will be
|
||||
#' reactive.
|
||||
#'
|
||||
#'
|
||||
#' @section Cache scope:
|
||||
#'
|
||||
#' By default, when `bindCache()` is used, it is scoped to the running
|
||||
#' application. That means that it shares a cache with all user sessions
|
||||
#' connected to the application (within the R process). This is done with the
|
||||
#' `cache` parameter's default value, `"app"`.
|
||||
#'
|
||||
#' With an app-level cache scope, one user can benefit from the work done for
|
||||
#' another user's session. In most cases, this is the best way to get
|
||||
#' performance improvements from caching. However, in some cases, this could
|
||||
#' leak information between sessions. For example, if the cache key does not
|
||||
#' fully encompass the inputs used by the value, then data could leak between
|
||||
#' the sessions. Or if a user sees that a cached reactive returns its value
|
||||
#' very quickly, they may be able to infer that someone else has already used
|
||||
#' it with the same values.
|
||||
#'
|
||||
#' It is also possible to scope the cache to the session, with
|
||||
#' `cache="session"`. This removes the risk of information leaking between
|
||||
#' sessions, but then one session cannot benefit from computations performed in
|
||||
#' another session.
|
||||
#'
|
||||
#' It is possible to pass in caching objects directly to
|
||||
#' `bindCache()`. This can be useful if, for example, you want to use a
|
||||
#' particular type of cache with specific cached reactives, or if you want to
|
||||
#' use a [cachem::cache_disk()] that is shared across multiple processes and
|
||||
#' persists beyond the current R session.
|
||||
#'
|
||||
#' To use different settings for an application-scoped cache, you can call
|
||||
#' [shinyOptions()] at the top of your app.R, server.R, or
|
||||
#' global.R. For example, this will create a cache with 500 MB of space
|
||||
#' instead of the default 200 MB:
|
||||
#'
|
||||
#' ```
|
||||
#' shinyOptions(cache = cachem::cache_mem(max_size = 500e6))
|
||||
#' ```
|
||||
#'
|
||||
#' To use different settings for a session-scoped cache, you can set
|
||||
#' `self$cache` at the top of your server function. By default, it will create
|
||||
#' 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
|
||||
#' `bindCache()` with `cache="session"`. This will create a 100 MB cache for
|
||||
#' the session:
|
||||
#'
|
||||
#' ```
|
||||
#' function(input, output, session) {
|
||||
#' session$cache <- cachem::cache_mem(max_size = 100e6)
|
||||
#' ...
|
||||
#' }
|
||||
#' ```
|
||||
#'
|
||||
#' If you want to use a cache that is shared across multiple R processes, you
|
||||
#' can use a [cachem::cache_disk()]. You can create a application-level shared
|
||||
#' 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"))
|
||||
#' ```
|
||||
#'
|
||||
#' This will create a subdirectory in your system temp directory named
|
||||
#' `myapp-cache` (replace `myapp-cache` with a unique name of
|
||||
#' your choosing). On most platforms, this directory will be removed when
|
||||
#' your system reboots. This cache will persist across multiple starts and
|
||||
#' stops of the R process, as long as you do not reboot.
|
||||
#'
|
||||
#' To have the cache persist even across multiple reboots, you can create the
|
||||
#' cache in a location outside of the temp directory. For example, it could
|
||||
#' be a subdirectory of the application:
|
||||
#'
|
||||
#' ```
|
||||
#' shinyOptions(cache = cachem::cache_disk("./myapp-cache"))
|
||||
#' ```
|
||||
#'
|
||||
#' In this case, resetting the cache will have to be done manually, by deleting
|
||||
#' the directory.
|
||||
#'
|
||||
#' You can also scope a cache to just one item, or selected items. To do that,
|
||||
#' create a [cachem::cache_mem()] or [cachem::cache_disk()], and pass it
|
||||
#' as the `cache` argument of `bindCache()`.
|
||||
#'
|
||||
|
||||
#'
|
||||
#' @section Computing cache keys:
|
||||
#'
|
||||
#' The actual cache key that is used internally takes value from evaluating
|
||||
#' the key expression(s) (from the `...` arguments) and combines it with the
|
||||
#' (unevaluated) value expression.
|
||||
#'
|
||||
#' This means that if there are two cached reactives which have the same
|
||||
#' result from evaluating the key, but different value expressions, then they
|
||||
#' will not need to worry about collisions.
|
||||
#'
|
||||
#' However, if two cached reactives have identical key and value expressions
|
||||
#' expressions, they will share the cached values. This is useful when using
|
||||
#' `cache="app"`: there may be multiple user sessions which create separate
|
||||
#' cached reactive objects (because they are created from the same code in the
|
||||
#' server function, but the server function is executed once for each user
|
||||
#' session), and those cached reactive objects across sessions can share
|
||||
#' values in the cache.
|
||||
|
||||
|
||||
|
||||
#'
|
||||
#' @section Async with cached reactives:
|
||||
#'
|
||||
#' With a cached reactive expression, the key and/or value expression can be
|
||||
#' _asynchronous_. In other words, they can be promises --- not regular R
|
||||
#' promises, but rather objects provided by the
|
||||
#' \href{https://rstudio.github.io/promises/}{\pkg{promises}} package, which
|
||||
#' are similar to promises in JavaScript. (See [promises::promise()] for more
|
||||
#' information.) You can also use [future::future()] objects to run code in a
|
||||
#' separate process or even on a remote machine.
|
||||
#'
|
||||
#' If the value returns a promise, then anything that consumes the cached
|
||||
#' reactive must expect it to return a promise.
|
||||
#'
|
||||
#' Similarly, if the key is a promise (in other words, if it is asynchronous),
|
||||
#' then the entire cached reactive must be asynchronous, since the key must be
|
||||
#' computed asynchronously before it knows whether to compute the value or the
|
||||
#' value is retrieved from the cache. Anything that consumes the cached
|
||||
#' reactive must therefore expect it to return a promise.
|
||||
#'
|
||||
|
||||
#'
|
||||
#' @section Developing render functions for caching:
|
||||
#'
|
||||
#' If you've implemented your own `render*()` function, it may just work with
|
||||
#' `bindCache()`, but it is possible that you will need to make some
|
||||
#' modifications. These modifications involve helping `bindCache()` avoid
|
||||
#' cache collisions, dealing with internal state that may be set by the,
|
||||
#' `render` function, and modifying the data as it goes in and comes out of
|
||||
#' the cache.
|
||||
#'
|
||||
#' You may need to provide a `cacheHint` to [createRenderFunction()] (or
|
||||
#' [htmlwidgets::shinyRenderWidget()], if you've authored an htmlwidget) in
|
||||
#' order for `bindCache()` to correctly compute a cache key.
|
||||
#'
|
||||
#' The potential problem is a cache collision. Consider the following:
|
||||
#'
|
||||
#' ```
|
||||
#' output$x1 <- renderText({ input$x }) %>% bindCache(input$x)
|
||||
#' output$x2 <- renderText({ input$x * 2 }) %>% bindCache(input$x)
|
||||
#' ```
|
||||
#'
|
||||
#' Both `output$x1` and `output$x2` use `input$x` as part of their cache key,
|
||||
#' but if it were the only thing used in the cache key, then the two outputs
|
||||
#' would have a cache collision, and they would have the same output. To avoid
|
||||
#' this, a _cache hint_ is automatically added when [renderText()] calls
|
||||
#' [createRenderFunction()]. The cache hint is used as part of the actual
|
||||
#' cache key, in addition to the one passed to `bindCache()` by the user. The
|
||||
#' cache hint can be viewed by calling the internal Shiny function
|
||||
#' `extractCacheHint()`:
|
||||
#'
|
||||
#' ```
|
||||
#' r <- renderText({ input$x })
|
||||
#' shiny:::extractCacheHint(r)
|
||||
#' ```
|
||||
#'
|
||||
#' This returns a nested list containing an item, `$origUserFunc$body`, which
|
||||
#' in this case is the expression which was passed to `renderText()`:
|
||||
#' `{ input$x }`. This (quoted) expression is mixed into the actual cache
|
||||
#' key, and it is how `output$x1` does not have collisions with `output$x2`.
|
||||
#'
|
||||
#' For most developers of render functions, nothing extra needs to be done;
|
||||
#' the automatic inference of the cache hint is sufficient. Again, you can
|
||||
#' check it by calling `shiny:::extractCacheHint()`, and by testing the
|
||||
#' render function for cache collisions in a real application.
|
||||
#'
|
||||
#' In some cases, however, the automatic cache hint inference is not
|
||||
#' sufficient, and it is necessary to provide a cache hint. This is true
|
||||
#' for `renderPrint()`. Unlike `renderText()`, it wraps the user-provided
|
||||
#' expression in another function, before passing it to [markRenderFunction()]
|
||||
#' (instead of [createRenderFunction()]). Because the user code is wrapped in
|
||||
#' another function, `markRenderFunction()` is not able to automatically
|
||||
#' extract the user-provided code and use it in the cache key. Instead,
|
||||
#' `renderPrint` calls `markRenderFunction()`, it explicitly passes along a
|
||||
#' `cacheHint`, which includes a label and the original user expression.
|
||||
#'
|
||||
#' In general, if you need to provide a `cacheHint`, it is best practice to
|
||||
#' provide a `label` id, the user's `expr`, as well as any other arguments
|
||||
#' that may influence the final value.
|
||||
#'
|
||||
#' For \pkg{htmlwidgets}, it will try to automatically infer a cache hint;
|
||||
#' again, you can inspect the cache hint with `shiny:::extractCacheHint()` and
|
||||
#' also test it in an application. If you do need to explicitly provide a
|
||||
#' cache hint, pass it to `shinyRenderWidget`. For example:
|
||||
#'
|
||||
#' ```
|
||||
#' renderMyWidget <- function(expr) {
|
||||
#' expr <- substitute(expr)
|
||||
#'
|
||||
#' htmlwidgets::shinyRenderWidget(expr,
|
||||
#' myWidgetOutput,
|
||||
#' quoted = TRUE,
|
||||
#' env = parent.frame(),
|
||||
#' cacheHint = list(label = "myWidget", userExpr = expr)
|
||||
#' )
|
||||
#' }
|
||||
#' ```
|
||||
#'
|
||||
#' If your `render` function sets any internal state, you may find it useful
|
||||
#' in your call to [createRenderFunction()] or [markRenderFunction()] to use
|
||||
#' the `cacheWriteHook` and/or `cacheReadHook` parameters. These hooks are
|
||||
#' 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
|
||||
#' that is stored and retrieved; this can be useful if extra information needs
|
||||
#' to be stored in the cache. They can also be used to modify the state of the
|
||||
#' application; for example, it can call [createWebDependency()] to make
|
||||
#' JS/CSS resources available if the cached object is loaded in a different R
|
||||
#' process. (See the source of `htmlwidgets::shinyRenderWidget` for an example
|
||||
#' of this.)
|
||||
#'
|
||||
#' @section Uncacheable objects:
|
||||
#'
|
||||
#' Some render functions cannot be cached, typically because they have side
|
||||
#' effects or modify some external state, and they must re-execute each time
|
||||
#' in order to work properly.
|
||||
#'
|
||||
#' For developers of such code, they should call [createRenderFunction()] or
|
||||
#' [markRenderFunction()] with `cacheHint = FALSE`.
|
||||
#'
|
||||
#'
|
||||
#' @section Caching with `renderPlot()`:
|
||||
#'
|
||||
#' When `bindCache()` is used with `renderPlot()`, the `height` and `width`
|
||||
#' passed to the original `renderPlot()` are ignored. They are superseded by
|
||||
#' `sizePolicy` argument passed to `bindCache. The default is:
|
||||
#'
|
||||
#' ```
|
||||
#' sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2)
|
||||
#' ```
|
||||
#'
|
||||
#' `sizePolicy` must be a function that takes a two-element numeric vector as
|
||||
#' input, representing the width and height of the `<img>` element in the
|
||||
#' browser window, and it must return a two-element numeric vector, representing
|
||||
#' the pixel dimensions of the plot to generate. The purpose is to round the
|
||||
#' actual pixel dimensions from the browser to some other dimensions, so that
|
||||
#' this will not generate and cache images of every possible pixel dimension.
|
||||
#' See [sizeGrowthRatio()] for more information on the default sizing policy.
|
||||
#'
|
||||
#' @param x The object to add caching to.
|
||||
#' @param ... One or more expressions to use in the caching key.
|
||||
#' @param cache The scope of the cache, or a cache object. This can be `"app"`
|
||||
#' (the default), `"session"`, or a cache object like a
|
||||
#' [cachem::cache_disk()]. See the Cache Scoping section for more information.
|
||||
#'
|
||||
#' @seealso [bindEvent()], [renderCachedPlot()] for caching plots.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' rc <- bindCache(
|
||||
#' x = reactive({
|
||||
#' Sys.sleep(2) # Pretend this is expensive
|
||||
#' input$x * 100
|
||||
#' }),
|
||||
#' input$x
|
||||
#' )
|
||||
#'
|
||||
#' # Can make it prettier with the %>% operator
|
||||
#' library(magrittr)
|
||||
#'
|
||||
#' rc <- reactive({
|
||||
#' Sys.sleep(2)
|
||||
#' input$x * 100
|
||||
#' }) %>%
|
||||
#' bindCache(input$x)
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' ## Only run app examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' # Basic example
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' sliderInput("x", "x", 1, 10, 5),
|
||||
#' sliderInput("y", "y", 1, 10, 5),
|
||||
#' div("x * y: "),
|
||||
#' verbatimTextOutput("txt")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' r <- reactive({
|
||||
#' # The value expression is an _expensive_ computation
|
||||
#' message("Doing expensive computation...")
|
||||
#' Sys.sleep(2)
|
||||
#' input$x * input$y
|
||||
#' }) %>%
|
||||
#' bindCache(input$x, input$y)
|
||||
#'
|
||||
#' output$txt <- renderText(r())
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # Caching renderText
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' sliderInput("x", "x", 1, 10, 5),
|
||||
#' sliderInput("y", "y", 1, 10, 5),
|
||||
#' div("x * y: "),
|
||||
#' verbatimTextOutput("txt")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$txt <- renderText({
|
||||
#' message("Doing expensive computation...")
|
||||
#' Sys.sleep(2)
|
||||
#' input$x * input$y
|
||||
#' }) %>%
|
||||
#' bindCache(input$x, input$y)
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # Demo of using events and caching with an actionButton
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' sliderInput("x", "x", 1, 10, 5),
|
||||
#' sliderInput("y", "y", 1, 10, 5),
|
||||
#' actionButton("go", "Go"),
|
||||
#' div("x * y: "),
|
||||
#' verbatimTextOutput("txt")
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' r <- reactive({
|
||||
#' message("Doing expensive computation...")
|
||||
#' Sys.sleep(2)
|
||||
#' input$x * input$y
|
||||
#' }) %>%
|
||||
#' bindCache(input$x, input$y) %>%
|
||||
#' bindEvent(input$go)
|
||||
#' # 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$y for the cache key, but doesn't take a reactive depdency on
|
||||
#' # them, because the reactive dependency is superseded by addEvent().
|
||||
#'
|
||||
#' output$txt <- renderText(r())
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#' }
|
||||
#'
|
||||
#' @export
|
||||
bindCache <- function(x, ..., cache = "app") {
|
||||
force(cache)
|
||||
|
||||
UseMethod("bindCache")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.default <- function(x, ...) {
|
||||
stop("Don't know how to handle object with class ", paste(class(x), collapse = ", "))
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.reactiveExpr <- function(x, ..., cache = "app") {
|
||||
check_dots_unnamed()
|
||||
|
||||
label <- exprToLabel(substitute(key), "cachedReactive")
|
||||
domain <- reactive_get_domain(x)
|
||||
|
||||
# Convert the ... to a function that returns their evaluated values.
|
||||
keyFunc <- quos_to_func(enquos0(...))
|
||||
|
||||
valueFunc <- reactive_get_value_func(x)
|
||||
# Hash cache hint now -- this will be added to the key later on, to reduce the
|
||||
# chance of key collisions with other cachedReactives.
|
||||
cacheHint <- rlang::hash(extractCacheHint(x))
|
||||
valueFunc <- wrapFunctionLabel(valueFunc, "cachedReactiveValueFunc", ..stacktraceon = TRUE)
|
||||
|
||||
# Don't hold on to the reference for x, so that it can be GC'd
|
||||
rm(x)
|
||||
# Hacky workaround for issue with `%>%` preventing GC:
|
||||
# https://github.com/tidyverse/magrittr/issues/229
|
||||
if (exists(".GenericCallEnv") && exists(".", envir = .GenericCallEnv)) {
|
||||
rm(list = ".", envir = .GenericCallEnv)
|
||||
}
|
||||
|
||||
|
||||
res <- reactive(label = label, domain = domain, {
|
||||
cache <- resolve_cache_object(cache, domain)
|
||||
hybrid_chain(
|
||||
keyFunc(),
|
||||
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook = identity, cacheWriteHook = identity)
|
||||
)
|
||||
})
|
||||
|
||||
class(res) <- c("reactive.cache", class(res))
|
||||
res
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.shiny.render.function <- function(x, ..., cache = "app") {
|
||||
check_dots_unnamed()
|
||||
|
||||
keyFunc <- quos_to_func(enquos0(...))
|
||||
|
||||
cacheHint <- rlang::hash(extractCacheHint(x))
|
||||
|
||||
cacheWriteHook <- attr(x, "cacheWriteHook", exact = TRUE) %||% identity
|
||||
cacheReadHook <- attr(x, "cacheReadHook", exact = TRUE) %||% identity
|
||||
|
||||
valueFunc <- x
|
||||
|
||||
renderFunc <- function(...) {
|
||||
domain <- getDefaultReactiveDomain()
|
||||
cache <- resolve_cache_object(cache, domain)
|
||||
|
||||
hybrid_chain(
|
||||
keyFunc(),
|
||||
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook, cacheWriteHook, ...)
|
||||
)
|
||||
}
|
||||
|
||||
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
|
||||
class(renderFunc) <- c("shiny.render.function.cache", class(valueFunc))
|
||||
renderFunc
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.shiny.renderPlot <- function(x, ...,
|
||||
cache = "app",
|
||||
sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2))
|
||||
{
|
||||
check_dots_unnamed()
|
||||
|
||||
valueFunc <- x
|
||||
|
||||
# Given the actual width/height of the image element in the browser, the
|
||||
# resize observer computes the width/height using sizePolicy() and pushes
|
||||
# those values into `fitWidth` and `fitHeight`. It's done this way so that the
|
||||
# `fitWidth` and `fitHeight` only change (and cause invalidations of the key
|
||||
# expression) when the rendered image size changes, and not every time the
|
||||
# browser's <img> tag changes size.
|
||||
#
|
||||
# If the key expression were invalidated every time the image element changed
|
||||
# size, even if the resulting key was the same (because `sizePolicy()` gave
|
||||
# the same output for a slightly different img element size), it would result
|
||||
# in getting the (same) image from the cache and sending it to the client
|
||||
# again. This resize observer prevents that.
|
||||
fitDims <- reactiveVal(NULL)
|
||||
resizeObserverCreated <- FALSE
|
||||
outputName <- NULL
|
||||
ensureResizeObserver <- function() {
|
||||
if (resizeObserverCreated)
|
||||
return()
|
||||
|
||||
doResizeCheck <- function() {
|
||||
if (is.null(outputName)) {
|
||||
outputName <<- getCurrentOutputInfo()$name
|
||||
}
|
||||
session <- getDefaultReactiveDomain()
|
||||
|
||||
width <- session$clientData[[paste0('output_', outputName, '_width')]] %||% 0
|
||||
height <- session$clientData[[paste0('output_', outputName, '_height')]] %||% 0
|
||||
|
||||
rect <- sizePolicy(c(width, height))
|
||||
fitDims(list(width = rect[1], height = rect[2]))
|
||||
}
|
||||
|
||||
# Run it once immediately, then set up the observer
|
||||
isolate(doResizeCheck())
|
||||
|
||||
observe({
|
||||
doResizeCheck()
|
||||
})
|
||||
# TODO: Make sure this observer gets GC'd if output$foo is replaced.
|
||||
# Currently, if you reassign output$foo, the observer persists until the
|
||||
# session ends. This is generally bad programming practice and should be
|
||||
# rare, but still, we should try to clean up properly.
|
||||
|
||||
resizeObserverCreated <<- TRUE
|
||||
}
|
||||
|
||||
renderFunc <- function(...) {
|
||||
hybrid_chain(
|
||||
# Pass in fitDims so that so that the generated plot will be the
|
||||
# dimensions specified by the sizePolicy; otherwise the plot would be the
|
||||
# exact dimensions of the img element, which isn't what we want for cached
|
||||
# plots.
|
||||
valueFunc(..., get_dims = fitDims),
|
||||
function(img) {
|
||||
# Replace exact pixel dimensions; instead, the max-height and max-width
|
||||
# will be set to 100% from CSS.
|
||||
img$class <- "shiny-scalable"
|
||||
img$width <- NULL
|
||||
img$height <- NULL
|
||||
|
||||
img
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
|
||||
class(renderFunc) <- class(valueFunc)
|
||||
|
||||
bindCache.shiny.render.function(
|
||||
renderFunc,
|
||||
...,
|
||||
{
|
||||
ensureResizeObserver()
|
||||
session <- getDefaultReactiveDomain()
|
||||
if (is.null(session) || is.null(fitDims())) {
|
||||
req(FALSE)
|
||||
}
|
||||
pixelratio <- session$clientData$pixelratio %||% 1
|
||||
|
||||
list(fitDims(), pixelratio)
|
||||
},
|
||||
cache = cache
|
||||
)
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.reactive.cache <- function(x, ...) {
|
||||
stop("bindCache() has already been called on the object.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.shiny.render.function.cache <- bindCache.reactive.cache
|
||||
|
||||
#' @export
|
||||
bindCache.reactive.event <- function(x, ...) {
|
||||
stop("Can't call bindCache() after calling bindEvent() on an object. Maybe you wanted to call bindEvent() after bindCache()?")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.shiny.render.function.event <- bindCache.reactive.event
|
||||
|
||||
#' @export
|
||||
bindCache.Observer <- function(x, ...) {
|
||||
stop("Can't bindCache an observer, because observers exist for the side efects, not for their return values.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindCache.function <- function(x, ...) {
|
||||
stop(
|
||||
"Don't know how to add caching to a plain function. ",
|
||||
"If this is a render* function for Shiny, it may need to be updated. ",
|
||||
"Please see ?shiny::bindCache for more information."
|
||||
)
|
||||
}
|
||||
|
||||
# Returns a function which should be passed as a step in to hybrid_chain(). The
|
||||
# returned function takes a cache key as input and manages storing and retrieving
|
||||
# values from the cache, as well as executing the valueFunc if needed.
|
||||
generateCacheFun <- function(
|
||||
valueFunc,
|
||||
cache,
|
||||
cacheHint,
|
||||
cacheReadHook,
|
||||
cacheWriteHook,
|
||||
...
|
||||
) {
|
||||
function(cacheKeyResult) {
|
||||
key_str <- rlang::hash(list(cacheKeyResult, cacheHint))
|
||||
res <- cache$get(key_str)
|
||||
|
||||
# Case 1: cache hit
|
||||
if (!is.key_missing(res)) {
|
||||
return(hybrid_chain(
|
||||
{
|
||||
# The first step is just to convert `res` to a promise or not, so
|
||||
# that hybrid_chain() knows to propagate the promise-ness.
|
||||
if (res$is_promise) promise_resolve(res)
|
||||
else res
|
||||
},
|
||||
function(res) {
|
||||
if (res$error) {
|
||||
stop(res$value)
|
||||
}
|
||||
|
||||
cacheReadHook(valueWithVisible(res))
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
# Case 2: cache miss
|
||||
#
|
||||
# valueFunc() might return a promise, or an actual value. Normally we'd
|
||||
# use a hybrid_chain() for this, but in this case, we need to have
|
||||
# different behavior if it's a promise or not a promise -- the
|
||||
# information about whether or not it's a promise needs to be stored in
|
||||
# the cache. We need to handle both cases and record in the cache
|
||||
# whether it's a promise or not, so that any consumer of the
|
||||
# cachedReactive() will be given the correct kind of object (a promise
|
||||
# vs. an actual value) in the case of a future cache hit.
|
||||
p <- withCallingHandlers(
|
||||
withVisible(isolate(valueFunc(...))),
|
||||
error = function(e) {
|
||||
cache$set(key_str, list(
|
||||
is_promise = FALSE,
|
||||
value = e,
|
||||
visible = TRUE,
|
||||
error = TRUE
|
||||
))
|
||||
}
|
||||
)
|
||||
|
||||
if (is.promising(p$value)) {
|
||||
p$value <- as.promise(p$value)
|
||||
p$value <- p$value$
|
||||
then(function(value) {
|
||||
res <- withVisible(value)
|
||||
cache$set(key_str, list(
|
||||
is_promise = TRUE,
|
||||
value = cacheWriteHook(res$value),
|
||||
visible = res$visible,
|
||||
error = FALSE
|
||||
))
|
||||
valueWithVisible(res)
|
||||
})$
|
||||
catch(function(e) {
|
||||
cache$set(key_str, list(
|
||||
is_promise = TRUE,
|
||||
value = e,
|
||||
visible = TRUE,
|
||||
error = TRUE
|
||||
))
|
||||
stop(e)
|
||||
})
|
||||
valueWithVisible(p)
|
||||
} else {
|
||||
# result is an ordinary value, not a promise.
|
||||
cache$set(key_str, list(
|
||||
is_promise = FALSE,
|
||||
value = cacheWriteHook(p$value),
|
||||
visible = p$visible,
|
||||
error = FALSE
|
||||
))
|
||||
return(valueWithVisible(p))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extractCacheHint <- function(func) {
|
||||
cacheHint <- attr(func, "cacheHint", exact = TRUE)
|
||||
|
||||
if (is_false(cacheHint)) {
|
||||
stop(
|
||||
"Cannot call `bindCache()` on this object because it is marked as not cacheable.",
|
||||
call. = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
if (is.null(cacheHint)) {
|
||||
warning("No cacheHint found for this object. ",
|
||||
"Caching may not work properly.")
|
||||
}
|
||||
|
||||
cacheHint
|
||||
}
|
||||
315
R/bind-event.R
Normal file
315
R/bind-event.R
Normal file
@@ -0,0 +1,315 @@
|
||||
#' Make an object respond only to specified reactive events
|
||||
#'
|
||||
#' @description
|
||||
#'
|
||||
#' Modify an object to respond to "event-like" reactive inputs, values, and
|
||||
#' expressions. `bindEvent()` can be used with reactive expressions, render
|
||||
#' functions, and observers. The resulting object takes a reactive dependency on
|
||||
#' the `...` arguments, and not on the original object's code. This can, for
|
||||
#' example, be used to make an observer execute only when a button is pressed.
|
||||
#'
|
||||
#' `bindEvent()` was added in Shiny 1.6.0. When it is used with [reactive()] and
|
||||
#' [observe()], it does the same thing as [eventReactive()] and
|
||||
#' [observeEvent()]. However, `bindEvent()` is more flexible: it can be combined
|
||||
#' with [bindCache()], and it can also be used with `render` functions (like
|
||||
#' [renderText()] and [renderPlot()]).
|
||||
#'
|
||||
#' @section Details:
|
||||
#'
|
||||
#' Shiny's reactive programming framework is primarily designed for calculated
|
||||
#' values (reactive expressions) and side-effect-causing actions (observers)
|
||||
#' that respond to *any* of their inputs changing. That's often what is
|
||||
#' desired in Shiny apps, but not always: sometimes you want to wait for a
|
||||
#' specific action to be taken from the user, like clicking an
|
||||
#' [actionButton()], before calculating an expression or taking an action. A
|
||||
#' reactive value or expression that is used to trigger other calculations in
|
||||
#' this way is called an *event*.
|
||||
#'
|
||||
#' These situations demand a more imperative, "event handling" style of
|
||||
#' programming that is possible--but not particularly intuitive--using the
|
||||
#' reactive programming primitives [observe()] and [isolate()]. `bindEvent()`
|
||||
#' provides a straightforward API for event handling that wraps `observe` and
|
||||
#' `isolate`.
|
||||
#'
|
||||
#' The `...` arguments are captured as expressions and combined into an
|
||||
#' **event expression**. When this event expression is invalidated (when its
|
||||
#' upstream reactive inputs change), that is an **event**, and it will cause
|
||||
#' the original object's code to execute.
|
||||
#'
|
||||
#' Use `bindEvent()` with `observe()` whenever you want to *perform an action*
|
||||
#' in response to an event. (This does the same thing as [observeEvent()],
|
||||
#' which was available in Shiny prior to version 1.6.0.) Note that
|
||||
#' "recalculate a value" does not generally count as performing an action --
|
||||
#' use [reactive()] for that.
|
||||
#'
|
||||
#' Use `bindEvent()` with `reactive()` to create a *calculated value* that
|
||||
#' only updates in response to an event. This is just like a normal [reactive
|
||||
#' expression][reactive] except it ignores all the usual invalidations that
|
||||
#' come from its reactive dependencies; it only invalidates in response to the
|
||||
#' given event. (This does the same thing as [eventReactive()], which was
|
||||
#' available in Shiny prior to version 1.6.0.)
|
||||
#'
|
||||
#' `bindEvent()` is often used with [bindCache()].
|
||||
#'
|
||||
#' @section ignoreNULL and ignoreInit:
|
||||
#'
|
||||
#' `bindEvent()` takes an `ignoreNULL` parameter that affects behavior when
|
||||
#' the event expression evaluates to `NULL` (or in the special case of an
|
||||
#' [actionButton()], `0`). In these cases, if `ignoreNULL` is `TRUE`, then it
|
||||
#' will raise a silent [validation][validate] error. This is useful behavior
|
||||
#' if you don't want to do the action or calculation when your app first
|
||||
#' starts, but wait for the user to initiate the action first (like a "Submit"
|
||||
#' button); whereas `ignoreNULL=FALSE` is desirable if you want to initially
|
||||
#' perform the action/calculation and just let the user re-initiate it (like a
|
||||
#' "Recalculate" button).
|
||||
#'
|
||||
#' `bindEvent()` also takes an `ignoreInit` argument. By default, reactive
|
||||
#' expressions and observers will run on the first reactive flush after they
|
||||
#' are created (except if, at that moment, the event expression evaluates to
|
||||
#' `NULL` and `ignoreNULL` is `TRUE`). But when responding to a click of an
|
||||
#' action button, it may often be useful to set `ignoreInit` to `TRUE`. For
|
||||
#' example, if you're setting up an observer to respond to a dynamically
|
||||
#' created button, then `ignoreInit = TRUE` will guarantee that the action
|
||||
#' will only be triggered when the button is actually clicked, instead of also
|
||||
#' being triggered when it is created/initialized. Similarly, if you're
|
||||
#' setting up a reactive that responds to a dynamically created button used to
|
||||
#' refresh some data (which is then returned by that `reactive`), then you
|
||||
#' should use `reactive(...) %>% bindEvent(..., ignoreInit = TRUE)` if you
|
||||
#' want to let the user decide if/when they want to refresh the data (since,
|
||||
#' depending on the app, this may be a computationally expensive operation).
|
||||
#'
|
||||
#' Even though `ignoreNULL` and `ignoreInit` can be used for similar purposes
|
||||
#' they are independent from one another. Here's the result of combining
|
||||
#' these:
|
||||
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`ignoreNULL = TRUE` and `ignoreInit = FALSE`}{
|
||||
#' This is the default. This combination means that reactive/observer code
|
||||
#' will run every time that event expression is not
|
||||
#' `NULL`. If, at the time of creation, the event expression happens
|
||||
#' to *not* be `NULL`, then the code runs.
|
||||
#' }
|
||||
#' \item{`ignoreNULL = FALSE` and `ignoreInit = FALSE`}{
|
||||
#' This combination means that reactive/observer code will
|
||||
#' run every time no matter what.
|
||||
#' }
|
||||
#' \item{`ignoreNULL = FALSE` and `ignoreInit = TRUE`}{
|
||||
#' This combination means that reactive/observer code will
|
||||
#' *not* run at the time of creation (because `ignoreInit = TRUE`),
|
||||
#' but it will run every other time.
|
||||
#' }
|
||||
#' \item{`ignoreNULL = TRUE` and `ignoreInit = TRUE`}{
|
||||
#' This combination means that reactive/observer code will
|
||||
#' *not* at the time of creation (because `ignoreInit = TRUE`).
|
||||
#' After that, the reactive/observer code will run every time that
|
||||
#' the event expression is not `NULL`.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
|
||||
#' @section Types of objects:
|
||||
#'
|
||||
#' `bindEvent()` can be used with reactive expressions, observers, and shiny
|
||||
#' render functions.
|
||||
#'
|
||||
#' When `bindEvent()` is used with `reactive()`, it creates a new reactive
|
||||
#' expression object.
|
||||
#'
|
||||
#' When `bindEvent()` is used with `observe()`, it alters the observer in
|
||||
#' place. It can only be used with observers which have not yet executed.
|
||||
#'
|
||||
#' @section Combining events and caching:
|
||||
#'
|
||||
#' In many cases, it makes sense to use `bindEvent()` along with
|
||||
#' `bindCache()`, because they each can reduce the amount of work done on the
|
||||
#' server. For example, you could have [sliderInput]s `x` and `y` and a
|
||||
#' `reactive()` that performs a time-consuming operation with those values.
|
||||
#' Using `bindCache()` can speed things up, especially if there are multiple
|
||||
#' users. But it might make sense to also not do the computation until the
|
||||
#' user sets both `x` and `y`, and then clicks on an [actionButton] named
|
||||
#' `go`.
|
||||
#'
|
||||
#' To use both caching and events, the object should first be passed to
|
||||
#' `bindCache()`, then `bindEvent()`. For example:
|
||||
|
||||
#'
|
||||
#' ```
|
||||
#' r <- reactive({
|
||||
#' Sys.sleep(2) # Pretend this is an expensive computation
|
||||
#' input$x * input$y
|
||||
#' }) %>%
|
||||
#' bindCache(input$x, input$y) %>%
|
||||
#' bindEvent(input$go)
|
||||
#' ```
|
||||
|
||||
#'
|
||||
#' Anything that consumes `r()` will take a reactive dependency on the event
|
||||
#' expression given to `bindEvent()`, and not the cache key expression given to
|
||||
#' `bindCache()`. In this case, it is just `input$go`.
|
||||
#'
|
||||
#' @param x An object to wrap so that is triggered only when a the specified
|
||||
#' event occurs.
|
||||
#' @param ignoreNULL Whether the action should be triggered (or value
|
||||
#' calculated) when the input is `NULL`. See Details.
|
||||
#' @param ignoreInit If `TRUE`, then, when the eventified object is first
|
||||
#' created/initialized, don't trigger the action or (compute the value). The
|
||||
#' default is `FALSE`. See Details.
|
||||
#' @param once Used only for observers. Whether this `observer` should be
|
||||
#' immediately destroyed after the first time that the code in the observer is
|
||||
#' run. This pattern is useful when you want to subscribe to a event that
|
||||
#' should only happen once.
|
||||
#' @param label A label for the observer or reactive, useful for debugging.
|
||||
#' @param ... One or more expressions that represents the event; this can be a
|
||||
#' simple reactive value like `input$click`, a call to a reactive expression
|
||||
#' like `dataset()`, or even a complex expression inside curly braces. If
|
||||
#' there are multiple expressions in the `...`, then it will take a dependency
|
||||
#' on all of them.
|
||||
#' @export
|
||||
bindEvent <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
|
||||
once = FALSE, label = NULL)
|
||||
{
|
||||
check_dots_unnamed()
|
||||
force(ignoreNULL)
|
||||
force(ignoreInit)
|
||||
force(once)
|
||||
|
||||
UseMethod("bindEvent")
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.default <- function(x, ...) {
|
||||
stop("Don't know how to handle object with class ", paste(class(x), collapse = ", "))
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.reactiveExpr <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
|
||||
label = NULL)
|
||||
{
|
||||
domain <- reactive_get_domain(x)
|
||||
|
||||
qs <- enquos0(...)
|
||||
eventFunc <- quos_to_func(qs)
|
||||
|
||||
valueFunc <- reactive_get_value_func(x)
|
||||
valueFunc <- wrapFunctionLabel(valueFunc, "eventReactiveValueFunc", ..stacktraceon = TRUE)
|
||||
|
||||
label <- label %||%
|
||||
sprintf('bindEvent(%s, %s)', attr(x, "observable", exact = TRUE)$.label, quos_to_label(qs))
|
||||
|
||||
# Don't hold on to the reference for x, so that it can be GC'd
|
||||
rm(x)
|
||||
|
||||
initialized <- FALSE
|
||||
|
||||
res <- reactive(label = label, domain = domain, ..stacktraceon = FALSE, {
|
||||
hybrid_chain(
|
||||
eventFunc(),
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
req(FALSE)
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(value))
|
||||
|
||||
isolate(valueFunc())
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
class(res) <- c("reactive.event", class(res))
|
||||
res
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.shiny.render.function <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE) {
|
||||
eventFunc <- quos_to_func(enquos0(...))
|
||||
|
||||
valueFunc <- x
|
||||
|
||||
initialized <- FALSE
|
||||
|
||||
renderFunc <- function(...) {
|
||||
hybrid_chain(
|
||||
eventFunc(),
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
req(FALSE)
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(value))
|
||||
|
||||
isolate(valueFunc(...))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
|
||||
class(renderFunc) <- c("shiny.render.function.event", class(valueFunc))
|
||||
renderFunc
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.Observer <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
|
||||
once = FALSE, label = NULL)
|
||||
{
|
||||
if (x$.execCount > 0) {
|
||||
stop("Cannot call bindEvent() on an Observer that has already been executed.")
|
||||
}
|
||||
|
||||
qs <- enquos0(...)
|
||||
eventFunc <- quos_to_func(qs)
|
||||
valueFunc <- x$.func
|
||||
|
||||
# Note that because the observer will already have been logged by this point,
|
||||
# this updated label won't show up in the reactlog.
|
||||
x$.label <- label %||% sprintf('bindEvent(%s, %s)', x$.label, quos_to_label(qs))
|
||||
|
||||
initialized <- FALSE
|
||||
|
||||
x$.func <- wrapFunctionLabel(
|
||||
name = x$.label,
|
||||
..stacktraceon = FALSE,
|
||||
func = function() {
|
||||
hybrid_chain(
|
||||
eventFunc(),
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
return()
|
||||
}
|
||||
|
||||
if (ignoreNULL && isNullEvent(value)) {
|
||||
return()
|
||||
}
|
||||
|
||||
if (once) {
|
||||
on.exit(x$destroy())
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(value))
|
||||
|
||||
isolate(valueFunc())
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
class(x) <- c("Observer.event", class(x))
|
||||
invisible(x)
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
bindEvent.reactive.event <- function(x, ...) {
|
||||
stop("bindEvent() has already been called on the object.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
bindEvent.Observer.event <- bindEvent.reactive.event
|
||||
@@ -1,6 +1,3 @@
|
||||
#' @include stack.R
|
||||
NULL
|
||||
|
||||
ShinySaveState <- R6Class("ShinySaveState",
|
||||
public = list(
|
||||
input = NULL,
|
||||
@@ -79,7 +76,7 @@ saveShinySaveState <- function(state) {
|
||||
|
||||
# Look for a save.interface function. This will be defined by the hosting
|
||||
# environment if it supports bookmarking.
|
||||
saveInterface <- getShinyOption("save.interface")
|
||||
saveInterface <- getShinyOption("save.interface", default = NULL)
|
||||
|
||||
if (is.null(saveInterface)) {
|
||||
if (inShinyServer()) {
|
||||
@@ -217,6 +214,22 @@ RestoreContext <- R6Class("RestoreContext",
|
||||
self$dir <- NULL
|
||||
},
|
||||
|
||||
# Completely replace the state
|
||||
set = function(active = FALSE, initErrorMessage = NULL, input = list(), values = list(), dir = NULL) {
|
||||
# Validate all inputs
|
||||
stopifnot(is.logical(active))
|
||||
stopifnot(is.null(initErrorMessage) || is.character(initErrorMessage))
|
||||
stopifnot(is.list(input))
|
||||
stopifnot(is.list(values))
|
||||
stopifnot(is.null(dir) || is.character(dir))
|
||||
|
||||
self$active <- active
|
||||
self$initErrorMessage <- initErrorMessage
|
||||
self$input <- RestoreInputSet$new(input)
|
||||
self$values <- list2env2(values, parent = emptyenv())
|
||||
self$dir <- dir
|
||||
},
|
||||
|
||||
# This should be called before a restore context is popped off the stack.
|
||||
flushPending = function() {
|
||||
self$input$flushPending()
|
||||
@@ -280,7 +293,7 @@ RestoreContext <- R6Class("RestoreContext",
|
||||
|
||||
# Look for a load.interface function. This will be defined by the hosting
|
||||
# environment if it supports bookmarking.
|
||||
loadInterface <- getShinyOption("load.interface")
|
||||
loadInterface <- getShinyOption("load.interface", default = NULL)
|
||||
|
||||
if (is.null(loadInterface)) {
|
||||
if (inShinyServer()) {
|
||||
@@ -431,8 +444,8 @@ RestoreInputSet <- R6Class("RestoreInputSet",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
restoreCtxStack <- Stack$new()
|
||||
# This is a fastmap::faststack(); value is assigned in .onLoad().
|
||||
restoreCtxStack <- NULL
|
||||
|
||||
withRestoreContext <- function(ctx, expr) {
|
||||
restoreCtxStack$push(ctx)
|
||||
@@ -453,7 +466,7 @@ hasCurrentRestoreContext <- function() {
|
||||
domain <- getDefaultReactiveDomain()
|
||||
if (!is.null(domain) && !is.null(domain$restoreContext))
|
||||
return(TRUE)
|
||||
|
||||
|
||||
return(FALSE)
|
||||
}
|
||||
|
||||
@@ -1144,10 +1157,10 @@ setBookmarkExclude <- function(names = character(0), session = getDefaultReactiv
|
||||
#' toupper(input$text)
|
||||
#' })
|
||||
#' onBookmark(function(state) {
|
||||
#' state$values$hash <- digest::digest(input$text, "md5")
|
||||
#' state$values$hash <- rlang::hash(input$text)
|
||||
#' })
|
||||
#' onRestore(function(state) {
|
||||
#' if (identical(digest::digest(input$text, "md5"), state$values$hash)) {
|
||||
#' if (identical(rlang::hash(input$text), state$values$hash)) {
|
||||
#' message("Module's input text matches hash ", state$values$hash)
|
||||
#' } else {
|
||||
#' message("Module's input text does not match hash ", state$values$hash)
|
||||
@@ -1170,10 +1183,10 @@ setBookmarkExclude <- function(names = character(0), session = getDefaultReactiv
|
||||
#' server <- function(input, output, session) {
|
||||
#' callModule(capitalizerServer, "tc")
|
||||
#' onBookmark(function(state) {
|
||||
#' state$values$hash <- digest::digest(input$text, "md5")
|
||||
#' state$values$hash <- rlang::hash(input$text)
|
||||
#' })
|
||||
#' onRestore(function(state) {
|
||||
#' if (identical(digest::digest(input$text, "md5"), state$values$hash)) {
|
||||
#' if (identical(rlang::hash(input$text), state$values$hash)) {
|
||||
#' message("App's input text matches hash ", state$values$hash)
|
||||
#' } else {
|
||||
#' message("App's input text does not match hash ", state$values$hash)
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
#' 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.
|
||||
#' @param theme Alternative Bootstrap stylesheet (normally a css file within the
|
||||
#' www directory). For example, to use the theme located at
|
||||
#' `www/bootstrap.css` you would use `theme = "bootstrap.css"`.
|
||||
#' @inheritParams bootstrapPage
|
||||
#'
|
||||
#' @return A UI defintion that can be passed to the [shinyUI] function.
|
||||
#'
|
||||
@@ -25,7 +23,7 @@
|
||||
#' higher-level layout functions like [sidebarLayout()].
|
||||
#'
|
||||
#' @note See the [
|
||||
#' Shiny-Application-Layout-Guide](http://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fluid
|
||||
#' Shiny-Application-Layout-Guide](https://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fluid
|
||||
#' pages.
|
||||
#'
|
||||
#' @family layout functions
|
||||
@@ -87,11 +85,12 @@
|
||||
#' }
|
||||
#' @rdname fluidPage
|
||||
#' @export
|
||||
fluidPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
|
||||
fluidPage <- function(..., title = NULL, responsive = deprecated(), theme = NULL, lang = NULL) {
|
||||
bootstrapPage(div(class = "container-fluid", ...),
|
||||
title = title,
|
||||
responsive = responsive,
|
||||
theme = theme)
|
||||
theme = theme,
|
||||
lang = lang)
|
||||
}
|
||||
|
||||
|
||||
@@ -115,9 +114,7 @@ fluidRow <- function(...) {
|
||||
#' @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 Alternative Bootstrap stylesheet (normally a css file within the
|
||||
#' www directory). For example, to use the theme located at
|
||||
#' `www/bootstrap.css` you would use `theme = "bootstrap.css"`.
|
||||
#' @inheritParams bootstrapPage
|
||||
#'
|
||||
#' @return A UI defintion that can be passed to the [shinyUI] function.
|
||||
#'
|
||||
@@ -128,7 +125,7 @@ fluidRow <- function(...) {
|
||||
#' with `fixedRow` and `column`.
|
||||
#'
|
||||
#' @note See the [
|
||||
#' Shiny Application Layout Guide](http://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fixed
|
||||
#' Shiny Application Layout Guide](https://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fixed
|
||||
#' pages.
|
||||
#'
|
||||
#' @family layout functions
|
||||
@@ -156,11 +153,12 @@ fluidRow <- function(...) {
|
||||
#'
|
||||
#' @rdname fixedPage
|
||||
#' @export
|
||||
fixedPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
|
||||
fixedPage <- function(..., title = NULL, responsive = deprecated(), theme = NULL, lang = NULL) {
|
||||
bootstrapPage(div(class = "container", ...),
|
||||
title = title,
|
||||
responsive = responsive,
|
||||
theme = theme)
|
||||
theme = theme,
|
||||
lang = lang)
|
||||
}
|
||||
|
||||
#' @rdname fixedPage
|
||||
@@ -355,6 +353,8 @@ sidebarLayout <- function(sidebarPanel,
|
||||
sidebarPanel <- function(..., width = 4) {
|
||||
div(class=paste0("col-sm-", width),
|
||||
tags$form(class="well",
|
||||
# A11y semantic landmark for sidebar
|
||||
role="complementary",
|
||||
...
|
||||
)
|
||||
)
|
||||
@@ -364,6 +364,8 @@ sidebarPanel <- function(..., width = 4) {
|
||||
#' @rdname sidebarLayout
|
||||
mainPanel <- function(..., width = 8) {
|
||||
div(class=paste0("col-sm-", width),
|
||||
# A11y semantic landmark for main region
|
||||
role="main",
|
||||
...
|
||||
)
|
||||
}
|
||||
@@ -432,7 +434,7 @@ verticalLayout <- function(..., fluid = TRUE) {
|
||||
flowLayout <- function(..., cellArgs = list()) {
|
||||
|
||||
children <- list(...)
|
||||
childIdx <- !nzchar(names(children) %OR% character(length(children)))
|
||||
childIdx <- !nzchar(names(children) %||% character(length(children)))
|
||||
attribs <- children[!childIdx]
|
||||
children <- children[childIdx]
|
||||
|
||||
@@ -515,7 +517,7 @@ inputPanel <- function(...) {
|
||||
splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
|
||||
|
||||
children <- list(...)
|
||||
childIdx <- !nzchar(names(children) %OR% character(length(children)))
|
||||
childIdx <- !nzchar(names(children) %||% character(length(children)))
|
||||
attribs <- children[!childIdx]
|
||||
children <- children[childIdx]
|
||||
count <- length(children)
|
||||
@@ -693,37 +695,3 @@ flexfill <- function(..., direction, flex, width = width, height = height) {
|
||||
)
|
||||
do.call(tags$div, c(attrs, divArgs))
|
||||
}
|
||||
|
||||
css <- function(..., collapse_ = "") {
|
||||
props <- list(...)
|
||||
if (length(props) == 0) {
|
||||
return("")
|
||||
}
|
||||
|
||||
if (is.null(names(props)) || any(names(props) == "")) {
|
||||
stop("cssList expects all arguments to be named")
|
||||
}
|
||||
|
||||
# Necessary to make factors show up as level names, not numbers
|
||||
props[] <- lapply(props, paste, collapse = " ")
|
||||
|
||||
# Drop null args
|
||||
props <- props[!sapply(props, empty)]
|
||||
if (length(props) == 0) {
|
||||
return("")
|
||||
}
|
||||
|
||||
# Replace all '.' and '_' in property names to '-'
|
||||
names(props) <- gsub("[._]", "-", tolower(gsub("([A-Z])", "-\\1", names(props))))
|
||||
|
||||
# Create "!important" suffix for each property whose name ends with !, then
|
||||
# remove the ! from the property name
|
||||
important <- ifelse(grepl("!$", names(props), perl = TRUE), " !important", "")
|
||||
names(props) <- sub("!$", "", names(props), perl = TRUE)
|
||||
|
||||
paste0(names(props), ":", props, important, ";", collapse = collapse_)
|
||||
}
|
||||
|
||||
empty <- function(x) {
|
||||
length(x) == 0 || (is.character(x) && !any(nzchar(x)))
|
||||
}
|
||||
|
||||
323
R/bootstrap.R
323
R/bootstrap.R
@@ -4,7 +4,7 @@ NULL
|
||||
#' Create a Bootstrap page
|
||||
#'
|
||||
#' Create a Shiny UI page that loads the CSS and JavaScript for
|
||||
#' [Bootstrap](http://getbootstrap.com/), and has no content in the page
|
||||
#' [Bootstrap](https://getbootstrap.com/), and has no content in the page
|
||||
#' body (other than what you provide).
|
||||
#'
|
||||
#' This function is primarily intended for users who are proficient in HTML/CSS,
|
||||
@@ -16,8 +16,15 @@ NULL
|
||||
#' @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 Alternative Bootstrap stylesheet (normally a css file within the
|
||||
#' www directory, e.g. `www/bootstrap.css`)
|
||||
#' @param theme One of the following:
|
||||
#' * `NULL` (the default), which implies a "stock" build of Bootstrap 3.
|
||||
#' * A [bslib::bs_theme()] object. This can be used to replace a stock
|
||||
#' build of Bootstrap 3 with a customized version of Bootstrap 3 or higher.
|
||||
#' * A character string pointing to an alternative Bootstrap stylesheet
|
||||
#' (normally a css file within the www directory, e.g. `www/bootstrap.css`).
|
||||
#' @param lang ISO 639-1 language code for the HTML page, such as "en" or "ko".
|
||||
#' 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.
|
||||
#'
|
||||
#' @return A UI defintion that can be passed to the [shinyUI] function.
|
||||
#'
|
||||
@@ -26,41 +33,185 @@ NULL
|
||||
#'
|
||||
#' @seealso [fluidPage()], [fixedPage()]
|
||||
#' @export
|
||||
bootstrapPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
|
||||
bootstrapPage <- function(..., title = NULL, responsive = deprecated(), theme = NULL, lang = NULL) {
|
||||
|
||||
if (!is.null(responsive)) {
|
||||
shinyDeprecated("The 'responsive' argument is no longer used with Bootstrap 3.")
|
||||
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."
|
||||
)
|
||||
}
|
||||
|
||||
attachDependencies(
|
||||
tagList(
|
||||
if (!is.null(title)) tags$head(tags$title(title)),
|
||||
if (!is.null(theme)) {
|
||||
tags$head(tags$link(rel="stylesheet", type="text/css", href = theme))
|
||||
},
|
||||
|
||||
# remainder of tags passed to the function
|
||||
list(...)
|
||||
),
|
||||
bootstrapLib()
|
||||
args <- list(
|
||||
if (!is.null(title)) tags$head(tags$title(title)),
|
||||
if (is.character(theme)) {
|
||||
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))
|
||||
},
|
||||
# remainder of tags passed to the function
|
||||
list(...)
|
||||
)
|
||||
|
||||
# If theme is a bslib::bs_theme() object, bootstrapLib() needs to come first
|
||||
# (so other tags, when rendered via tagFunction(), know about the relevant
|
||||
# theme). However, if theme is anything else, we intentionally avoid changing
|
||||
# the tagList() contents to avoid breaking user code that makes assumptions
|
||||
# about the return value https://github.com/rstudio/shiny/issues/3235
|
||||
if (is_bs_theme(theme)) {
|
||||
args <- c(bootstrapLib(theme), args)
|
||||
ui <- do.call(tagList, args)
|
||||
} else {
|
||||
ui <- do.call(tagList, args)
|
||||
ui <- attachDependencies(ui, bootstrapLib())
|
||||
}
|
||||
|
||||
setLang(ui, lang)
|
||||
}
|
||||
|
||||
setLang <- function(ui, lang) {
|
||||
# Add lang attribute to be passed to renderPage function
|
||||
attr(ui, "lang") <- lang
|
||||
ui
|
||||
}
|
||||
getLang <- function(ui) {
|
||||
# Check if ui has lang attribute; otherwise, NULL
|
||||
attr(ui, "lang", exact = TRUE)
|
||||
}
|
||||
|
||||
#' Bootstrap libraries
|
||||
#'
|
||||
#' This function returns a set of web dependencies necessary for using Bootstrap
|
||||
#' This function defines a set of web dependencies necessary for using Bootstrap
|
||||
#' components in a web page.
|
||||
#'
|
||||
#' It isn't necessary to call this function if you use
|
||||
#' [bootstrapPage()] or others which use `bootstrapPage`, such
|
||||
#' [basicPage()], [fluidPage()], [fillPage()],
|
||||
#' [pageWithSidebar()], and [navbarPage()], because they
|
||||
#' already include the Bootstrap web dependencies.
|
||||
#' It isn't necessary to call this function if you use [bootstrapPage()] or
|
||||
#' others which use `bootstrapPage`, such [fluidPage()], [navbarPage()],
|
||||
#' [fillPage()], etc, because they already include the Bootstrap web dependencies.
|
||||
#'
|
||||
#' @inheritParams bootstrapPage
|
||||
#' @export
|
||||
bootstrapLib <- function(theme = NULL) {
|
||||
htmlDependency("bootstrap", "3.4.1",
|
||||
tagFunction(function() {
|
||||
# If we're not compiling Bootstrap Sass (from bslib), return the
|
||||
# static Bootstrap build.
|
||||
if (!is_bs_theme(theme)) {
|
||||
# We'll enter here if `theme` is the path to a .css file, like that
|
||||
# provided by `shinythemes::shinytheme("darkly")`.
|
||||
return(bootstrapDependency(theme))
|
||||
}
|
||||
|
||||
# Make bootstrap Sass available so other tagFunction()s (e.g.,
|
||||
# sliderInput() et al) can resolve their HTML dependencies at render time
|
||||
# using getCurrentTheme(). Note that we're making an implicit assumption
|
||||
# that this tagFunction() executes *before* all other tagFunction()s; but
|
||||
# that should be fine considering that, DOM tree order is preorder,
|
||||
# depth-first traversal, and at least in the bootstrapPage(theme) case, we
|
||||
# have control over the relative ordering.
|
||||
# https://dom.spec.whatwg.org/#concept-tree
|
||||
# https://stackoverflow.com/a/16113998/1583084
|
||||
#
|
||||
# Note also that since this is shinyOptions() (and not options()), the
|
||||
# option is automatically reset when the app (or session) exits
|
||||
if (isRunning()) {
|
||||
setCurrentTheme(theme)
|
||||
registerThemeDependency(bs_theme_deps)
|
||||
|
||||
} else {
|
||||
# Technically, this a potential issue (someone trying to execute/render
|
||||
# bootstrapLib outside of a Shiny app), but it seems that, in that case,
|
||||
# you likely have other problems, since sliderInput() et al. already assume
|
||||
# that Shiny is the one doing the rendering
|
||||
#warning(
|
||||
# "It appears `shiny::bootstrapLib()` was rendered outside of an Shiny ",
|
||||
# "application context, likely by calling `as.tags()`, `as.character()`, ",
|
||||
# "or `print()` directly on `bootstrapLib()` or UI components that may ",
|
||||
# "depend on it (e.g., `fluidPage()`, etc). For 'themable' UI components ",
|
||||
# "(e.g., `sliderInput()`, `selectInput()`, `dateInput()`, etc) to style ",
|
||||
# "themselves based on the Bootstrap theme, make sure `bootstrapLib()` is ",
|
||||
# "provided directly to the UI and that the UI is provided direction to ",
|
||||
# "`shinyApp()` (or `runApp()`)", call. = FALSE
|
||||
#)
|
||||
}
|
||||
|
||||
bslib::bs_theme_dependencies(theme)
|
||||
})
|
||||
}
|
||||
|
||||
# This is defined outside of bootstrapLib() because registerThemeDependency()
|
||||
# wants a non-anonymous function with a single argument
|
||||
bs_theme_deps <- function(theme) {
|
||||
bslib::bs_theme_dependencies(theme)
|
||||
}
|
||||
|
||||
is_bs_theme <- function(x) {
|
||||
is_available("bslib", "0.2.0.9000") &&
|
||||
bslib::is_bs_theme(x)
|
||||
}
|
||||
|
||||
#' Obtain Shiny's Bootstrap Sass theme
|
||||
#'
|
||||
#' Intended for use by Shiny developers to create Shiny bindings with intelligent
|
||||
#' styling based on the [bootstrapLib()]'s `theme` value.
|
||||
#'
|
||||
#' @return If called at render-time (i.e., inside a [htmltools::tagFunction()]),
|
||||
#' and [bootstrapLib()]'s `theme` has been set to a [bslib::bs_theme()]
|
||||
#' object, then this returns the `theme`. Otherwise, this returns `NULL`.
|
||||
#' @seealso [getCurrentOutputInfo()], [bootstrapLib()], [htmltools::tagFunction()]
|
||||
#'
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
getCurrentTheme <- function() {
|
||||
getShinyOption("bootstrapTheme", default = NULL)
|
||||
}
|
||||
|
||||
setCurrentTheme <- function(theme) {
|
||||
shinyOptions(bootstrapTheme = theme)
|
||||
}
|
||||
|
||||
#' Register a theme dependency
|
||||
#'
|
||||
#' This function registers a function that returns an [htmlDependency()] or list
|
||||
#' of such objects. If `session$setCurrentTheme()` is called, the function will
|
||||
#' be re-executed, and the resulting html dependency will be sent to the client.
|
||||
#'
|
||||
#' Note that `func` should **not** be an anonymous function, or a function which
|
||||
#' is defined within the calling function. This is so that,
|
||||
#' `registerThemeDependency()` is called multiple times with the function, it
|
||||
#' tries to deduplicate them
|
||||
#'
|
||||
#' @param func A function that takes one argument, `theme` (which is a
|
||||
#' [sass::sass_layer()] object), and returns an htmlDependency object, or list
|
||||
#' of them.
|
||||
#'
|
||||
#' @export
|
||||
#' @keywords internal
|
||||
registerThemeDependency <- function(func) {
|
||||
func_expr <- substitute(func)
|
||||
if (is.call(func_expr) && identical(func_expr[[1]], as.symbol("function"))) {
|
||||
warning("`func` should not be an anonymous function. ",
|
||||
"It should be declared outside of the function that calls registerThemeDependency(); ",
|
||||
"otherwise it will not be deduplicated by Shiny and multiple copies of the ",
|
||||
"resulting htmlDependency may be computed and sent to the client.")
|
||||
}
|
||||
if (!is.function(func) || length(formals(func)) != 1) {
|
||||
stop("`func` must be a function with one argument (the current theme)")
|
||||
}
|
||||
|
||||
# Note that this will automatically scope to the app or session level,
|
||||
# depending on if this is called from within a session or not.
|
||||
funcs <- getShinyOption("themeDependencyFuncs", default = list())
|
||||
|
||||
# Don't add func if it's already present.
|
||||
have_func <- any(vapply(funcs, identical, logical(1), func))
|
||||
if (!have_func) {
|
||||
funcs[[length(funcs) + 1]] <- func
|
||||
}
|
||||
|
||||
shinyOptions("themeDependencyFuncs" = funcs)
|
||||
}
|
||||
|
||||
bootstrapDependency <- function(theme) {
|
||||
htmlDependency(
|
||||
"bootstrap", "3.4.1",
|
||||
c(
|
||||
href = "shared/bootstrap",
|
||||
file = system.file("www/shared/bootstrap", package = "shiny")
|
||||
@@ -70,15 +221,16 @@ bootstrapLib <- function(theme = NULL) {
|
||||
# Safely adding accessibility plugin for screen readers and keyboard users; no break for sighted aspects (see https://github.com/paypal/bootstrap-accessibility-plugin)
|
||||
"accessibility/js/bootstrap-accessibility.min.js"
|
||||
),
|
||||
stylesheet = if (is.null(theme)) c(
|
||||
"css/bootstrap.min.css",
|
||||
stylesheet = c(
|
||||
theme %||% "css/bootstrap.min.css",
|
||||
# Safely adding accessibility plugin for screen readers and keyboard users; no break for sighted aspects (see https://github.com/paypal/bootstrap-accessibility-plugin)
|
||||
"accessibility/css/bootstrap-accessibility.css"
|
||||
"accessibility/css/bootstrap-accessibility.min.css"
|
||||
),
|
||||
meta = list(viewport = "width=device-width, initial-scale=1")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' @rdname bootstrapPage
|
||||
#' @export
|
||||
basicPage <- function(...) {
|
||||
@@ -131,7 +283,7 @@ basicPage <- function(...) {
|
||||
#' @param title The title to use for the browser window/tab (it will not be
|
||||
#' shown in the document).
|
||||
#' @param bootstrap If `TRUE`, load the Bootstrap CSS library.
|
||||
#' @param theme URL to alternative Bootstrap stylesheet.
|
||||
#' @inheritParams bootstrapPage
|
||||
#'
|
||||
#' @family layout functions
|
||||
#'
|
||||
@@ -159,7 +311,7 @@ basicPage <- function(...) {
|
||||
#' )
|
||||
#' @export
|
||||
fillPage <- function(..., padding = 0, title = NULL, bootstrap = TRUE,
|
||||
theme = NULL) {
|
||||
theme = NULL, lang = NULL) {
|
||||
|
||||
fillCSS <- tags$head(tags$style(type = "text/css",
|
||||
"html, body { width: 100%; height: 100%; overflow: hidden; }",
|
||||
@@ -167,14 +319,18 @@ fillPage <- function(..., padding = 0, title = NULL, bootstrap = TRUE,
|
||||
))
|
||||
|
||||
if (isTRUE(bootstrap)) {
|
||||
bootstrapPage(title = title, theme = theme, fillCSS, ...)
|
||||
ui <- bootstrapPage(title = title, theme = theme, fillCSS, lang = lang, ...)
|
||||
} else {
|
||||
tagList(
|
||||
ui <- tagList(
|
||||
fillCSS,
|
||||
if (!is.null(title)) tags$head(tags$title(title)),
|
||||
...
|
||||
)
|
||||
|
||||
ui <- setLang(ui, lang)
|
||||
}
|
||||
|
||||
return(ui)
|
||||
}
|
||||
|
||||
collapseSizes <- function(padding) {
|
||||
@@ -221,11 +377,9 @@ collapseSizes <- function(padding) {
|
||||
#' layout.
|
||||
#' @param responsive This option is deprecated; it is no longer optional with
|
||||
#' Bootstrap 3.
|
||||
#' @param theme Alternative Bootstrap stylesheet (normally a css file within the
|
||||
#' www directory). For example, to use the theme located at
|
||||
#' `www/bootstrap.css` you would use `theme = "bootstrap.css"`.
|
||||
#' @param windowTitle The title that should be displayed by the browser window.
|
||||
#' Useful if `title` is not a string.
|
||||
#' @inheritParams bootstrapPage
|
||||
#' @param icon Optional icon to appear on a `navbarMenu` tab.
|
||||
#'
|
||||
#' @return A UI defintion that can be passed to the [shinyUI] function.
|
||||
@@ -266,14 +420,15 @@ navbarPage <- function(title,
|
||||
footer = NULL,
|
||||
inverse = FALSE,
|
||||
collapsible = FALSE,
|
||||
collapsable,
|
||||
collapsable = deprecated(),
|
||||
fluid = TRUE,
|
||||
responsive = NULL,
|
||||
responsive = deprecated(),
|
||||
theme = NULL,
|
||||
windowTitle = title) {
|
||||
windowTitle = title,
|
||||
lang = NULL) {
|
||||
|
||||
if (!missing(collapsable)) {
|
||||
shinyDeprecated("`collapsable` is deprecated; use `collapsible` instead.")
|
||||
if (lifecycle::is_present(collapsable)) {
|
||||
shinyDeprecated("0.10.2.2", "navbarPage(collapsable =)", "navbarPage(collapsible =)")
|
||||
collapsible <- collapsable
|
||||
}
|
||||
|
||||
@@ -341,6 +496,7 @@ navbarPage <- function(title,
|
||||
title = windowTitle,
|
||||
responsive = responsive,
|
||||
theme = theme,
|
||||
lang = lang,
|
||||
tags$nav(class=navbarClass, role="navigation", containerDiv),
|
||||
contentDiv
|
||||
)
|
||||
@@ -586,11 +742,12 @@ tabsetPanel <- function(...,
|
||||
id = NULL,
|
||||
selected = NULL,
|
||||
type = c("tabs", "pills", "hidden"),
|
||||
position = NULL) {
|
||||
if (!is.null(position)) {
|
||||
shinyDeprecated(msg = paste("tabsetPanel: argument 'position' is deprecated;",
|
||||
"it has been discontinued in Bootstrap 3."),
|
||||
version = "0.10.2.2")
|
||||
position = deprecated()) {
|
||||
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."
|
||||
)
|
||||
}
|
||||
|
||||
if (!is.null(id))
|
||||
@@ -721,7 +878,7 @@ findAndMarkSelectedTab <- function(tabs, selected, foundSelected) {
|
||||
foundSelected <<- TRUE
|
||||
div <- markTabAsSelected(div)
|
||||
} else {
|
||||
tabValue <- div$attribs$`data-value` %OR% div$attribs$title
|
||||
tabValue <- div$attribs$`data-value` %||% div$attribs$title
|
||||
if (identical(selected, tabValue)) {
|
||||
foundSelected <<- TRUE
|
||||
div <- markTabAsSelected(div)
|
||||
@@ -848,15 +1005,15 @@ buildTabItem <- function(index, tabsetId, foundSelected, tabs = NULL,
|
||||
#' Render a reactive output variable as text within an application page.
|
||||
#' `textOutput()` is usually paired with [renderText()] and puts regular text
|
||||
#' in `<div>` or `<span>`; `verbatimTextOutput()` is usually paired with
|
||||
#' [renderPrint()] and provudes fixed-width text in a `<pre>`.
|
||||
#' [renderPrint()] and provides fixed-width text in a `<pre>`.
|
||||
#'
|
||||
#' In both funtions, text is HTML-escaped prior to rendering.
|
||||
#' In both functions, text is HTML-escaped prior to rendering.
|
||||
#'
|
||||
#' @param outputId output variable to read the value from
|
||||
#' @param container a function to generate an HTML element to contain the text
|
||||
#' @param inline use an inline (`span()`) or block container (`div()`)
|
||||
#' for the output
|
||||
#' @return A output element for use in UI.
|
||||
#' @return An output element for use in UI.
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
@@ -879,14 +1036,14 @@ textOutput <- function(outputId, container = if (inline) span else div, inline =
|
||||
|
||||
#' @param placeholder if the output is empty or `NULL`, should an empty
|
||||
#' rectangle be displayed to serve as a placeholder? (does not affect
|
||||
#' behavior when the the output in nonempty)
|
||||
#' behavior when the output is nonempty)
|
||||
#' @export
|
||||
#' @rdname textOutput
|
||||
verbatimTextOutput <- function(outputId, placeholder = FALSE) {
|
||||
pre(id = outputId,
|
||||
class = paste(c("shiny-text-output", if (!placeholder) "noplaceholder"),
|
||||
collapse = " ")
|
||||
)
|
||||
class = "shiny-text-output",
|
||||
class = if (!placeholder) "noplaceholder"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -898,7 +1055,9 @@ imageOutput <- function(outputId, width = "100%", height="400px",
|
||||
inline = FALSE) {
|
||||
|
||||
style <- if (!inline) {
|
||||
paste("width:", validateCssUnit(width), ";", "height:", validateCssUnit(height))
|
||||
# Using `css()` here instead of paste/sprintf so that NULL values will
|
||||
# result in the property being dropped altogether
|
||||
css(width = validateCssUnit(width), height = validateCssUnit(height))
|
||||
}
|
||||
|
||||
|
||||
@@ -1197,49 +1356,8 @@ plotOutput <- function(outputId, width = "100%", height="400px",
|
||||
res
|
||||
}
|
||||
|
||||
#' Create a table output element
|
||||
#'
|
||||
#' Render a [renderTable()] or [renderDataTable()] within an
|
||||
#' application page. `renderTable` uses a standard HTML table, while
|
||||
#' `renderDataTable` uses the DataTables Javascript library to create an
|
||||
#' interactive table with more features.
|
||||
#'
|
||||
#' @param outputId output variable to read the table from
|
||||
#' @return A table output element that can be included in a panel
|
||||
#'
|
||||
#' @seealso [renderTable()], [renderDataTable()].
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' # table example
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' fluidRow(
|
||||
#' column(12,
|
||||
#' tableOutput('table')
|
||||
#' )
|
||||
#' )
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$table <- renderTable(iris)
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
#'
|
||||
#' # DataTables example
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' fluidRow(
|
||||
#' column(12,
|
||||
#' dataTableOutput('table')
|
||||
#' )
|
||||
#' )
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$table <- renderDataTable(iris)
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#' @rdname renderTable
|
||||
#' @export
|
||||
tableOutput <- function(outputId) {
|
||||
div(id = outputId, class="shiny-html-output")
|
||||
@@ -1257,7 +1375,7 @@ dataTableDependency <- list(
|
||||
)
|
||||
)
|
||||
|
||||
#' @rdname tableOutput
|
||||
#' @rdname renderDataTable
|
||||
#' @export
|
||||
dataTableOutput <- function(outputId) {
|
||||
attachDependencies(
|
||||
@@ -1376,20 +1494,21 @@ downloadLink <- function(outputId, label="Download", class=NULL, ...) {
|
||||
#' @param name Name of icon. Icons are drawn from the
|
||||
#' [Font Awesome Free](https://fontawesome.com/) (currently icons from
|
||||
#' the v5.13.0 set are supported with the v4 naming convention) and
|
||||
#' [Glyphicons](http://getbootstrap.com/components/#glyphicons)
|
||||
#' [Glyphicons](https://getbootstrap.com/components/#glyphicons)
|
||||
#' libraries. Note that the "fa-" and "glyphicon-" prefixes should not be used
|
||||
#' in icon names (i.e. the "fa-calendar" icon should be referred to as
|
||||
#' "calendar")
|
||||
#' @param class Additional classes to customize the style of the icon (see the
|
||||
#' [usage examples](http://fontawesome.io/examples/) for details on
|
||||
#' [usage examples](https://fontawesome.com/how-to-use) for details on
|
||||
#' supported styles).
|
||||
#' @param lib Icon library to use ("font-awesome" or "glyphicon")
|
||||
#' @param ... Arguments passed to the `<i>` tag of [htmltools::tags]
|
||||
#'
|
||||
#' @return An icon element
|
||||
#'
|
||||
#' @seealso For lists of available icons, see
|
||||
#' [http://fontawesome.io/icons/](http://fontawesome.io/icons/) and
|
||||
#' [http://getbootstrap.com/components/#glyphicons](http://getbootstrap.com/components/#glyphicons).
|
||||
#' [https://fontawesome.com/icons](https://fontawesome.com/icons) and
|
||||
#' [https://getbootstrap.com/components/#glyphicons](https://getbootstrap.com/components/#glyphicons).
|
||||
#'
|
||||
#'
|
||||
#' @examples
|
||||
@@ -1402,7 +1521,7 @@ downloadLink <- function(outputId, label="Download", class=NULL, ...) {
|
||||
#' tabPanel("Table", icon = icon("table"))
|
||||
#' )
|
||||
#' @export
|
||||
icon <- function(name, class = NULL, lib = "font-awesome") {
|
||||
icon <- function(name, class = NULL, lib = "font-awesome", ...) {
|
||||
prefixes <- list(
|
||||
"font-awesome" = "fa",
|
||||
"glyphicon" = "glyphicon"
|
||||
@@ -1428,7 +1547,7 @@ icon <- function(name, class = NULL, lib = "font-awesome") {
|
||||
if (!is.null(class))
|
||||
iconClass <- paste(iconClass, class)
|
||||
|
||||
iconTag <- tags$i(class = iconClass, role = "presentation", `aria-label` = paste(name, "icon"))
|
||||
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") {
|
||||
|
||||
567
R/cache-disk.R
567
R/cache-disk.R
@@ -1,567 +0,0 @@
|
||||
#' Create a disk cache object
|
||||
#'
|
||||
#' A disk cache object is a key-value store that saves the values as files in a
|
||||
#' directory on disk. Objects can be stored and retrieved using the `get()`
|
||||
#' and `set()` methods. Objects are automatically pruned from the cache
|
||||
#' according to the parameters `max_size`, `max_age`, `max_n`,
|
||||
#' and `evict`.
|
||||
#'
|
||||
#'
|
||||
#' @section Missing Keys:
|
||||
#'
|
||||
#' The `missing` and `exec_missing` parameters controls what happens
|
||||
#' when `get()` is called with a key that is not in the cache (a cache
|
||||
#' miss). The default behavior is to return a [key_missing()]
|
||||
#' object. This is a *sentinel value* that indicates that the key was not
|
||||
#' present in the cache. You can test if the returned value represents a
|
||||
#' missing key by using the [is.key_missing()] function. You can
|
||||
#' also have `get()` return a different sentinel value, like `NULL`.
|
||||
#' If you want to throw an error on a cache miss, you can do so by providing a
|
||||
#' function for `missing` that takes one argument, the key, and also use
|
||||
#' `exec_missing=TRUE`.
|
||||
#'
|
||||
#' When the cache is created, you can supply a value for `missing`, which
|
||||
#' sets the default value to be returned for missing values. It can also be
|
||||
#' overridden when `get()` is called, by supplying a `missing`
|
||||
#' argument. For example, if you use `cache$get("mykey", missing =
|
||||
#' NULL)`, it will return `NULL` if the key is not in the cache.
|
||||
#'
|
||||
#' If your cache is configured so that `get()` returns a sentinel value
|
||||
#' to represent a cache miss, then `set` will also not allow you to store
|
||||
#' the sentinel value in the cache. It will throw an error if you attempt to
|
||||
#' do so.
|
||||
#'
|
||||
#' Instead of returning the same sentinel value each time there is cache miss,
|
||||
#' the cache can execute a function each time `get()` encounters missing
|
||||
#' key. If the function returns a value, then `get()` will in turn return
|
||||
#' that value. However, a more common use is for the function to throw an
|
||||
#' error. If an error is thrown, then `get()` will not return a value.
|
||||
#'
|
||||
#' To do this, pass a one-argument function to `missing`, and use
|
||||
#' `exec_missing=TRUE`. For example, if you want to throw an error that
|
||||
#' prints the missing key, you could do this:
|
||||
#'
|
||||
#' \preformatted{
|
||||
#' diskCache(
|
||||
#' missing = function(key) {
|
||||
#' stop("Attempted to get missing key: ", key)
|
||||
#' },
|
||||
#' exec_missing = TRUE
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' If you use this, the code that calls `get()` should be wrapped with
|
||||
#' [tryCatch()] to gracefully handle missing keys.
|
||||
#'
|
||||
#' @section Cache pruning:
|
||||
#'
|
||||
#' Cache pruning occurs when `set()` is called, or it can be invoked
|
||||
#' manually by calling `prune()`.
|
||||
#'
|
||||
#' The disk cache will throttle the pruning so that it does not happen on
|
||||
#' every call to `set()`, because the filesystem operations for checking
|
||||
#' the status of files can be slow. Instead, it will prune once in every 20
|
||||
#' calls to `set()`, or if at least 5 seconds have elapsed since the last
|
||||
#' prune occurred, whichever is first. These parameters are currently not
|
||||
#' customizable, but may be in the future.
|
||||
#'
|
||||
#' When a pruning occurs, if there are any objects that are older than
|
||||
#' `max_age`, they will be removed.
|
||||
#'
|
||||
#' The `max_size` and `max_n` parameters are applied to the cache as
|
||||
#' a whole, in contrast to `max_age`, which is applied to each object
|
||||
#' individually.
|
||||
#'
|
||||
#' If the number of objects in the cache exceeds `max_n`, then objects
|
||||
#' will be removed from the cache according to the eviction policy, which is
|
||||
#' set with the `evict` parameter. Objects will be removed so that the
|
||||
#' number of items is `max_n`.
|
||||
#'
|
||||
#' If the size of the objects in the cache exceeds `max_size`, then
|
||||
#' objects will be removed from the cache. Objects will be removed from the
|
||||
#' cache so that the total size remains under `max_size`. Note that the
|
||||
#' size is calculated using the size of the files, not the size of disk space
|
||||
#' used by the files --- these two values can differ because of files are
|
||||
#' stored in blocks on disk. For example, if the block size is 4096 bytes,
|
||||
#' then a file that is one byte in size will take 4096 bytes on disk.
|
||||
#'
|
||||
#' Another time that objects can be removed from the cache is when
|
||||
#' `get()` is called. If the target object is older than `max_age`,
|
||||
#' it will be removed and the cache will report it as a missing value.
|
||||
#'
|
||||
#' @section Eviction policies:
|
||||
#'
|
||||
#' If `max_n` or `max_size` are used, then objects will be removed
|
||||
#' from the cache according to an eviction policy. The available eviction
|
||||
#' policies are:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`"lru"`}{
|
||||
#' Least Recently Used. The least recently used objects will be removed.
|
||||
#' This uses the filesystem's mtime property. When "lru" is used, each
|
||||
#' `get()` is called, it will update the file's mtime.
|
||||
#' }
|
||||
#' \item{`"fifo"`}{
|
||||
#' First-in-first-out. The oldest objects will be removed.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' Both of these policies use files' mtime. Note that some filesystems (notably
|
||||
#' FAT) have poor mtime resolution. (atime is not used because support for
|
||||
#' atime is worse than mtime.)
|
||||
#'
|
||||
#'
|
||||
#' @section Sharing among multiple processes:
|
||||
#'
|
||||
#' The directory for a DiskCache can be shared among multiple R processes. To
|
||||
#' do this, each R process should have a DiskCache object that uses the same
|
||||
#' directory. Each DiskCache will do pruning independently of the others, so if
|
||||
#' they have different pruning parameters, then one DiskCache may remove cached
|
||||
#' objects before another DiskCache would do so.
|
||||
#'
|
||||
#' Even though it is possible for multiple processes to share a DiskCache
|
||||
#' directory, this should not be done on networked file systems, because of
|
||||
#' slow performance of networked file systems can cause problems. If you need
|
||||
#' a high-performance shared cache, you can use one built on a database like
|
||||
#' Redis, SQLite, mySQL, or similar.
|
||||
#'
|
||||
#' When multiple processes share a cache directory, there are some potential
|
||||
#' race conditions. For example, if your code calls `exists(key)` to check
|
||||
#' if an object is in the cache, and then call `get(key)`, the object may
|
||||
#' be removed from the cache in between those two calls, and `get(key)`
|
||||
#' will throw an error. Instead of calling the two functions, it is better to
|
||||
#' simply call `get(key)`, and check that the returned object is not a
|
||||
#' `key_missing()` object, using `is.key_missing()`. This effectively tests for
|
||||
#' existence and gets the object in one operation.
|
||||
#'
|
||||
#' It is also possible for one processes to prune objects at the same time that
|
||||
#' another processes is trying to prune objects. If this happens, you may see
|
||||
#' a warning from `file.remove()` failing to remove a file that has
|
||||
#' already been deleted.
|
||||
#'
|
||||
#'
|
||||
#' @section Methods:
|
||||
#'
|
||||
#' A disk cache object has the following methods:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`get(key, missing, exec_missing)`}{
|
||||
#' Returns the value associated with `key`. If the key is not in the
|
||||
#' cache, then it returns the value specified by `missing` or,
|
||||
#' `missing` is a function and `exec_missing=TRUE`, then
|
||||
#' executes `missing`. The function can throw an error or return the
|
||||
#' value. If either of these parameters are specified here, then they
|
||||
#' will override the defaults that were set when the DiskCache object was
|
||||
#' created. See section Missing Keys for more information.
|
||||
#' }
|
||||
#' \item{`set(key, value)`}{
|
||||
#' Stores the `key`-`value` pair in the cache.
|
||||
#' }
|
||||
#' \item{`exists(key)`}{
|
||||
#' Returns `TRUE` if the cache contains the key, otherwise
|
||||
#' `FALSE`.
|
||||
#' }
|
||||
#' \item{`size()`}{
|
||||
#' Returns the number of items currently in the cache.
|
||||
#' }
|
||||
#' \item{`keys()`}{
|
||||
#' Returns a character vector of all keys currently in the cache.
|
||||
#' }
|
||||
#' \item{`reset()`}{
|
||||
#' Clears all objects from the cache.
|
||||
#' }
|
||||
#' \item{`destroy()`}{
|
||||
#' Clears all objects in the cache, and removes the cache directory from
|
||||
#' disk.
|
||||
#' }
|
||||
#' \item{`prune()`}{
|
||||
#' Prunes the cache, using the parameters specified by `max_size`,
|
||||
#' `max_age`, `max_n`, and `evict`.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @param dir Directory to store files for the cache. If `NULL` (the
|
||||
#' default) it will create and use a temporary directory.
|
||||
#' @param max_age Maximum age of files in cache before they are evicted, in
|
||||
#' seconds. Use `Inf` for no age limit.
|
||||
#' @param max_size Maximum size of the cache, in bytes. If the cache exceeds
|
||||
#' this size, cached objects will be removed according to the value of the
|
||||
#' `evict`. Use `Inf` for no size limit.
|
||||
#' @param max_n Maximum number of objects in the cache. If the number of objects
|
||||
#' exceeds this value, then cached objects will be removed according to the
|
||||
#' value of `evict`. Use `Inf` for no limit of number of items.
|
||||
#' @param evict The eviction policy to use to decide which objects are removed
|
||||
#' when a cache pruning occurs. Currently, `"lru"` and `"fifo"` are
|
||||
#' supported.
|
||||
#' @param destroy_on_finalize If `TRUE`, then when the DiskCache object is
|
||||
#' garbage collected, the cache directory and all objects inside of it will be
|
||||
#' deleted from disk. If `FALSE` (the default), it will do nothing when
|
||||
#' finalized.
|
||||
#' @param missing A value to return or a function to execute when
|
||||
#' `get(key)` is called but the key is not present in the cache. The
|
||||
#' default is a [key_missing()] object. If it is a function to
|
||||
#' execute, the function must take one argument (the key), and you must also
|
||||
#' use `exec_missing = TRUE`. If it is a function, it is useful in most
|
||||
#' cases for it to throw an error, although another option is to return a
|
||||
#' value. If a value is returned, that value will in turn be returned by
|
||||
#' `get()`. See section Missing keys for more information.
|
||||
#' @param exec_missing If `FALSE` (the default), then treat `missing`
|
||||
#' as a value to return when `get()` results in a cache miss. If
|
||||
#' `TRUE`, treat `missing` as a function to execute when
|
||||
#' `get()` results in a cache miss.
|
||||
#' @param logfile An optional filename or connection object to where logging
|
||||
#' information will be written. To log to the console, use `stdout()`.
|
||||
#'
|
||||
#' @export
|
||||
diskCache <- function(
|
||||
dir = NULL,
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
destroy_on_finalize = FALSE,
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
DiskCache$new(dir, max_size, max_age, max_n, evict, destroy_on_finalize,
|
||||
missing, exec_missing, logfile)
|
||||
}
|
||||
|
||||
|
||||
DiskCache <- R6Class("DiskCache",
|
||||
public = list(
|
||||
initialize = function(
|
||||
dir = NULL,
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
destroy_on_finalize = FALSE,
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
if (exec_missing && (!is.function(missing) || length(formals(missing)) == 0)) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
if (is.null(dir)) {
|
||||
dir <- tempfile("DiskCache-")
|
||||
}
|
||||
if (!is.numeric(max_size)) stop("max_size must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_age)) stop("max_age must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_n)) stop("max_n must be a number. Use `Inf` for no limit.")
|
||||
|
||||
if (!dirExists(dir)) {
|
||||
private$log(paste0("initialize: Creating ", dir))
|
||||
dir.create(dir, recursive = TRUE)
|
||||
}
|
||||
|
||||
private$dir <- normalizePath(dir)
|
||||
private$max_size <- max_size
|
||||
private$max_age <- max_age
|
||||
private$max_n <- max_n
|
||||
private$evict <- match.arg(evict)
|
||||
private$destroy_on_finalize <- destroy_on_finalize
|
||||
private$missing <- missing
|
||||
private$exec_missing <- exec_missing
|
||||
private$logfile <- logfile
|
||||
|
||||
private$prune_last_time <- as.numeric(Sys.time())
|
||||
},
|
||||
|
||||
get = function(key, missing = private$missing, exec_missing = private$exec_missing) {
|
||||
private$log(paste0('get: key "', key, '"'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
|
||||
private$maybe_prune_single(key)
|
||||
|
||||
filename <- private$key_to_filename(key)
|
||||
|
||||
# Instead of calling exists() before fetching the value, just try to
|
||||
# fetch the value. This reduces the risk of a race condition when
|
||||
# multiple processes share a cache.
|
||||
read_error <- FALSE
|
||||
tryCatch(
|
||||
{
|
||||
value <- suppressWarnings(readRDS(filename))
|
||||
if (private$evict == "lru"){
|
||||
Sys.setFileTime(filename, Sys.time())
|
||||
}
|
||||
},
|
||||
error = function(e) {
|
||||
read_error <<- TRUE
|
||||
}
|
||||
)
|
||||
if (read_error) {
|
||||
private$log(paste0('get: key "', key, '" is missing'))
|
||||
|
||||
if (exec_missing) {
|
||||
if (!is.function(missing) || length(formals(missing)) == 0) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
return(missing(key))
|
||||
} else {
|
||||
return(missing)
|
||||
}
|
||||
}
|
||||
|
||||
private$log(paste0('get: key "', key, '" found'))
|
||||
value
|
||||
},
|
||||
|
||||
set = function(key, value) {
|
||||
private$log(paste0('set: key "', key, '"'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
|
||||
file <- private$key_to_filename(key)
|
||||
temp_file <- paste0(file, "-temp-", createUniqueId(8))
|
||||
|
||||
save_error <- FALSE
|
||||
ref_object <- FALSE
|
||||
tryCatch(
|
||||
{
|
||||
saveRDS(value, file = temp_file,
|
||||
refhook = function(x) {
|
||||
ref_object <<- TRUE
|
||||
NULL
|
||||
}
|
||||
)
|
||||
file.rename(temp_file, file)
|
||||
},
|
||||
error = function(e) {
|
||||
save_error <<- TRUE
|
||||
# Unlike file.remove(), unlink() does not raise warning if file does
|
||||
# not exist.
|
||||
unlink(temp_file)
|
||||
}
|
||||
)
|
||||
if (save_error) {
|
||||
private$log(paste0('set: key "', key, '" error'))
|
||||
stop('Error setting value for key "', key, '".')
|
||||
}
|
||||
if (ref_object) {
|
||||
private$log(paste0('set: value is a reference object'))
|
||||
warning("A reference object was cached in a serialized format. The restored object may not work as expected.")
|
||||
}
|
||||
|
||||
private$prune_throttled()
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
exists = function(key) {
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
file.exists(private$key_to_filename(key))
|
||||
},
|
||||
|
||||
# Return all keys in the cache
|
||||
keys = function() {
|
||||
self$is_destroyed(throw = TRUE)
|
||||
files <- dir(private$dir, "\\.rds$")
|
||||
sub("\\.rds$", "", files)
|
||||
},
|
||||
|
||||
remove = function(key) {
|
||||
private$log(paste0('remove: key "', key, '"'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
validate_key(key)
|
||||
file.remove(private$key_to_filename(key))
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
reset = function() {
|
||||
private$log(paste0('reset'))
|
||||
self$is_destroyed(throw = TRUE)
|
||||
file.remove(dir(private$dir, "\\.rds$", full.names = TRUE))
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
prune = function() {
|
||||
# TODO: It would be good to add parameters `n` and `size`, so that the
|
||||
# cache can be pruned to `max_n - n` and `max_size - size` before adding
|
||||
# an object. Right now we prune after adding the object, so the cache
|
||||
# can temporarily grow past the limits. The reason we don't do this now
|
||||
# is because it is expensive to find the size of the serialized object
|
||||
# before adding it.
|
||||
|
||||
private$log('prune')
|
||||
self$is_destroyed(throw = TRUE)
|
||||
|
||||
current_time <- Sys.time()
|
||||
|
||||
filenames <- dir(private$dir, "\\.rds$", full.names = TRUE)
|
||||
info <- file.info(filenames)
|
||||
info <- info[info$isdir == FALSE, ]
|
||||
info$name <- rownames(info)
|
||||
rownames(info) <- NULL
|
||||
# Files could be removed between the dir() and file.info() calls. The
|
||||
# entire row for such files will have NA values. Remove those rows.
|
||||
info <- info[!is.na(info$size), ]
|
||||
|
||||
# 1. Remove any files where the age exceeds max age.
|
||||
if (is.finite(private$max_age)) {
|
||||
timediff <- as.numeric(current_time - info$mtime, units = "secs")
|
||||
rm_idx <- timediff > private$max_age
|
||||
if (any(rm_idx)) {
|
||||
private$log(paste0("prune max_age: Removing ", paste(info$name[rm_idx], collapse = ", ")))
|
||||
rm_success <- file.remove(info$name[rm_idx])
|
||||
# This maps rm_success back into the TRUEs in the rm_idx vector.
|
||||
# If (for example) rm_idx is c(F,T,F,T,T) and rm_success is c(T,F,T),
|
||||
# then this line modifies rm_idx to be c(F,T,F,F,T).
|
||||
rm_idx[rm_idx] <- rm_success
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
}
|
||||
|
||||
# Sort objects by priority. The sorting is done in a function which can be
|
||||
# called multiple times but only does the work the first time.
|
||||
info_is_sorted <- FALSE
|
||||
ensure_info_is_sorted <- function() {
|
||||
if (info_is_sorted) return()
|
||||
|
||||
info <<- info[order(info$mtime, decreasing = TRUE), ]
|
||||
info_is_sorted <<- TRUE
|
||||
}
|
||||
|
||||
# 2. Remove files if there are too many.
|
||||
if (is.finite(private$max_n) && nrow(info) > private$max_n) {
|
||||
ensure_info_is_sorted()
|
||||
rm_idx <- seq_len(nrow(info)) > private$max_n
|
||||
private$log(paste0("prune max_n: Removing ", paste(info$name[rm_idx], collapse = ", ")))
|
||||
rm_success <- file.remove(info$name[rm_idx])
|
||||
rm_idx[rm_idx] <- rm_success
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
|
||||
# 3. Remove files if cache is too large.
|
||||
if (is.finite(private$max_size) && sum(info$size) > private$max_size) {
|
||||
ensure_info_is_sorted()
|
||||
cum_size <- cumsum(info$size)
|
||||
rm_idx <- cum_size > private$max_size
|
||||
private$log(paste0("prune max_size: Removing ", paste(info$name[rm_idx], collapse = ", ")))
|
||||
rm_success <- file.remove(info$name[rm_idx])
|
||||
rm_idx[rm_idx] <- rm_success
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
|
||||
private$prune_last_time <- as.numeric(current_time)
|
||||
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
size = function() {
|
||||
self$is_destroyed(throw = TRUE)
|
||||
length(dir(private$dir, "\\.rds$"))
|
||||
},
|
||||
|
||||
destroy = function() {
|
||||
if (self$is_destroyed()) {
|
||||
return(invisible(self))
|
||||
}
|
||||
|
||||
private$log(paste0("destroy: Removing ", private$dir))
|
||||
# First create a sentinel file so that other processes sharing this
|
||||
# cache know that the cache is to be destroyed. This is needed because
|
||||
# the recursive unlink is not atomic: another process can add a file to
|
||||
# the directory after unlink starts removing files but before it removes
|
||||
# the directory, and when that happens, the directory removal will fail.
|
||||
file.create(file.path(private$dir, "__destroyed__"))
|
||||
# Remove all the .rds files. This will not remove the setinel file.
|
||||
file.remove(dir(private$dir, "\\.rds$", full.names = TRUE))
|
||||
# Next remove dir recursively, including sentinel file.
|
||||
unlink(private$dir, recursive = TRUE)
|
||||
private$destroyed <- TRUE
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
is_destroyed = function(throw = FALSE) {
|
||||
if (!dirExists(private$dir) ||
|
||||
file.exists(file.path(private$dir, "__destroyed__")))
|
||||
{
|
||||
# It's possible for another process to destroy a shared cache directory
|
||||
private$destroyed <- TRUE
|
||||
}
|
||||
|
||||
if (throw) {
|
||||
if (private$destroyed) {
|
||||
stop("Attempted to use cache which has been destroyed:\n ", private$dir)
|
||||
}
|
||||
|
||||
} else {
|
||||
private$destroyed
|
||||
}
|
||||
},
|
||||
|
||||
finalize = function() {
|
||||
if (private$destroy_on_finalize) {
|
||||
self$destroy()
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
dir = NULL,
|
||||
max_age = NULL,
|
||||
max_size = NULL,
|
||||
max_n = NULL,
|
||||
evict = NULL,
|
||||
destroy_on_finalize = NULL,
|
||||
destroyed = FALSE,
|
||||
missing = NULL,
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL,
|
||||
|
||||
prune_throttle_counter = 0,
|
||||
prune_last_time = NULL,
|
||||
|
||||
key_to_filename = function(key) {
|
||||
validate_key(key)
|
||||
# Additional validation. This 80-char limit is arbitrary, and is
|
||||
# intended to avoid hitting a filename length limit on Windows.
|
||||
if (nchar(key) > 80) {
|
||||
stop("Invalid key: key must have fewer than 80 characters.")
|
||||
}
|
||||
file.path(private$dir, paste0(key, ".rds"))
|
||||
},
|
||||
|
||||
# A wrapper for prune() that throttles it, because prune() can be
|
||||
# expensive due to filesystem operations. This function will prune only
|
||||
# once every 20 times it is called, or if it has been more than 5 seconds
|
||||
# since the last time the cache was actually pruned, whichever is first.
|
||||
# In the future, the behavior may be customizable.
|
||||
prune_throttled = function() {
|
||||
# Count the number of times prune() has been called.
|
||||
private$prune_throttle_counter <- private$prune_throttle_counter + 1
|
||||
|
||||
if (private$prune_throttle_counter > 20 ||
|
||||
private$prune_last_time - as.numeric(Sys.time()) > 5)
|
||||
{
|
||||
self$prune()
|
||||
private$prune_throttle_counter <- 0
|
||||
}
|
||||
},
|
||||
|
||||
# Prunes a single object if it exceeds max_age. If the object does not
|
||||
# exceed max_age, or if the object doesn't exist, do nothing.
|
||||
maybe_prune_single = function(key) {
|
||||
obj <- private$cache[[key]]
|
||||
if (is.null(obj)) return()
|
||||
|
||||
timediff <- as.numeric(Sys.time()) - obj$mtime
|
||||
if (timediff > private$max_age) {
|
||||
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
|
||||
rm(list = key, envir = private$cache)
|
||||
}
|
||||
},
|
||||
|
||||
log = function(text) {
|
||||
if (is.null(private$logfile)) return()
|
||||
|
||||
text <- paste0(format(Sys.time(), "[%Y-%m-%d %H:%M:%OS3] DiskCache "), text)
|
||||
cat(text, sep = "\n", file = private$logfile, append = TRUE)
|
||||
}
|
||||
)
|
||||
)
|
||||
365
R/cache-memory.R
365
R/cache-memory.R
@@ -1,365 +0,0 @@
|
||||
#' Create a memory cache object
|
||||
#'
|
||||
#' A memory cache object is a key-value store that saves the values in an
|
||||
#' environment. Objects can be stored and retrieved using the `get()` and
|
||||
#' `set()` methods. Objects are automatically pruned from the cache
|
||||
#' according to the parameters `max_size`, `max_age`, `max_n`,
|
||||
#' and `evict`.
|
||||
#'
|
||||
#' In a `MemoryCache`, R objects are stored directly in the cache; they are
|
||||
#' not *not* serialized before being stored in the cache. This contrasts
|
||||
#' with other cache types, like [diskCache()], where objects are
|
||||
#' serialized, and the serialized object is cached. This can result in some
|
||||
#' differences of behavior. For example, as long as an object is stored in a
|
||||
#' MemoryCache, it will not be garbage collected.
|
||||
#'
|
||||
#'
|
||||
#' @section Missing keys:
|
||||
#' The `missing` and `exec_missing` parameters controls what happens
|
||||
#' when `get()` is called with a key that is not in the cache (a cache
|
||||
#' miss). The default behavior is to return a [key_missing()]
|
||||
#' object. This is a *sentinel value* that indicates that the key was not
|
||||
#' present in the cache. You can test if the returned value represents a
|
||||
#' missing key by using the [is.key_missing()] function. You can
|
||||
#' also have `get()` return a different sentinel value, like `NULL`.
|
||||
#' If you want to throw an error on a cache miss, you can do so by providing a
|
||||
#' function for `missing` that takes one argument, the key, and also use
|
||||
#' `exec_missing=TRUE`.
|
||||
#'
|
||||
#' When the cache is created, you can supply a value for `missing`, which
|
||||
#' sets the default value to be returned for missing values. It can also be
|
||||
#' overridden when `get()` is called, by supplying a `missing`
|
||||
#' argument. For example, if you use `cache$get("mykey", missing =
|
||||
#' NULL)`, it will return `NULL` if the key is not in the cache.
|
||||
#'
|
||||
#' If your cache is configured so that `get()` returns a sentinel value
|
||||
#' to represent a cache miss, then `set` will also not allow you to store
|
||||
#' the sentinel value in the cache. It will throw an error if you attempt to
|
||||
#' do so.
|
||||
#'
|
||||
#' Instead of returning the same sentinel value each time there is cache miss,
|
||||
#' the cache can execute a function each time `get()` encounters missing
|
||||
#' key. If the function returns a value, then `get()` will in turn return
|
||||
#' that value. However, a more common use is for the function to throw an
|
||||
#' error. If an error is thrown, then `get()` will not return a value.
|
||||
#'
|
||||
#' To do this, pass a one-argument function to `missing`, and use
|
||||
#' `exec_missing=TRUE`. For example, if you want to throw an error that
|
||||
#' prints the missing key, you could do this:
|
||||
#'
|
||||
#' \preformatted{
|
||||
#' diskCache(
|
||||
#' missing = function(key) {
|
||||
#' stop("Attempted to get missing key: ", key)
|
||||
#' },
|
||||
#' exec_missing = TRUE
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' If you use this, the code that calls `get()` should be wrapped with
|
||||
#' [tryCatch()] to gracefully handle missing keys.
|
||||
#'
|
||||
#' @section Cache pruning:
|
||||
#'
|
||||
#' Cache pruning occurs when `set()` is called, or it can be invoked
|
||||
#' manually by calling `prune()`.
|
||||
#'
|
||||
#' When a pruning occurs, if there are any objects that are older than
|
||||
#' `max_age`, they will be removed.
|
||||
#'
|
||||
#' The `max_size` and `max_n` parameters are applied to the cache as
|
||||
#' a whole, in contrast to `max_age`, which is applied to each object
|
||||
#' individually.
|
||||
#'
|
||||
#' If the number of objects in the cache exceeds `max_n`, then objects
|
||||
#' will be removed from the cache according to the eviction policy, which is
|
||||
#' set with the `evict` parameter. Objects will be removed so that the
|
||||
#' number of items is `max_n`.
|
||||
#'
|
||||
#' If the size of the objects in the cache exceeds `max_size`, then
|
||||
#' objects will be removed from the cache. Objects will be removed from the
|
||||
#' cache so that the total size remains under `max_size`. Note that the
|
||||
#' size is calculated using the size of the files, not the size of disk space
|
||||
#' used by the files --- these two values can differ because of files are
|
||||
#' stored in blocks on disk. For example, if the block size is 4096 bytes,
|
||||
#' then a file that is one byte in size will take 4096 bytes on disk.
|
||||
#'
|
||||
#' Another time that objects can be removed from the cache is when
|
||||
#' `get()` is called. If the target object is older than `max_age`,
|
||||
#' it will be removed and the cache will report it as a missing value.
|
||||
#'
|
||||
#' @section Eviction policies:
|
||||
#'
|
||||
#' If `max_n` or `max_size` are used, then objects will be removed
|
||||
#' from the cache according to an eviction policy. The available eviction
|
||||
#' policies are:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`"lru"`}{
|
||||
#' Least Recently Used. The least recently used objects will be removed.
|
||||
#' This uses the filesystem's atime property. Some filesystems do not
|
||||
#' support atime, or have a very low atime resolution. The DiskCache will
|
||||
#' check for atime support, and if the filesystem does not support atime,
|
||||
#' a warning will be issued and the "fifo" policy will be used instead.
|
||||
#' }
|
||||
#' \item{`"fifo"`}{
|
||||
#' First-in-first-out. The oldest objects will be removed.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @section Methods:
|
||||
#'
|
||||
#' A disk cache object has the following methods:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`get(key, missing, exec_missing)`}{
|
||||
#' Returns the value associated with `key`. If the key is not in the
|
||||
#' cache, then it returns the value specified by `missing` or,
|
||||
#' `missing` is a function and `exec_missing=TRUE`, then
|
||||
#' executes `missing`. The function can throw an error or return the
|
||||
#' value. If either of these parameters are specified here, then they
|
||||
#' will override the defaults that were set when the DiskCache object was
|
||||
#' created. See section Missing Keys for more information.
|
||||
#' }
|
||||
#' \item{`set(key, value)`}{
|
||||
#' Stores the `key`-`value` pair in the cache.
|
||||
#' }
|
||||
#' \item{`exists(key)`}{
|
||||
#' Returns `TRUE` if the cache contains the key, otherwise
|
||||
#' `FALSE`.
|
||||
#' }
|
||||
#' \item{`size()`}{
|
||||
#' Returns the number of items currently in the cache.
|
||||
#' }
|
||||
#' \item{`keys()`}{
|
||||
#' Returns a character vector of all keys currently in the cache.
|
||||
#' }
|
||||
#' \item{`reset()`}{
|
||||
#' Clears all objects from the cache.
|
||||
#' }
|
||||
#' \item{`destroy()`}{
|
||||
#' Clears all objects in the cache, and removes the cache directory from
|
||||
#' disk.
|
||||
#' }
|
||||
#' \item{`prune()`}{
|
||||
#' Prunes the cache, using the parameters specified by `max_size`,
|
||||
#' `max_age`, `max_n`, and `evict`.
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' @inheritParams diskCache
|
||||
#'
|
||||
#' @export
|
||||
memoryCache <- function(
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
MemoryCache$new(max_size, max_age, max_n, evict, missing, exec_missing, logfile)
|
||||
}
|
||||
|
||||
MemoryCache <- R6Class("MemoryCache",
|
||||
public = list(
|
||||
initialize = function(
|
||||
max_size = 10 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
missing = key_missing(),
|
||||
exec_missing = FALSE,
|
||||
logfile = NULL)
|
||||
{
|
||||
if (exec_missing && (!is.function(missing) || length(formals(missing)) == 0)) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
if (!is.numeric(max_size)) stop("max_size must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_age)) stop("max_age must be a number. Use `Inf` for no limit.")
|
||||
if (!is.numeric(max_n)) stop("max_n must be a number. Use `Inf` for no limit.")
|
||||
private$cache <- fastmap()
|
||||
private$max_size <- max_size
|
||||
private$max_age <- max_age
|
||||
private$max_n <- max_n
|
||||
private$evict <- match.arg(evict)
|
||||
private$missing <- missing
|
||||
private$exec_missing <- exec_missing
|
||||
private$logfile <- logfile
|
||||
},
|
||||
|
||||
get = function(key, missing = private$missing, exec_missing = private$exec_missing) {
|
||||
private$log(paste0('get: key "', key, '"'))
|
||||
validate_key(key)
|
||||
|
||||
private$maybe_prune_single(key)
|
||||
|
||||
if (!self$exists(key)) {
|
||||
private$log(paste0('get: key "', key, '" is missing'))
|
||||
if (exec_missing) {
|
||||
if (!is.function(missing) || length(formals(missing)) == 0) {
|
||||
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
|
||||
}
|
||||
return(missing(key))
|
||||
} else {
|
||||
return(missing)
|
||||
}
|
||||
}
|
||||
|
||||
private$log(paste0('get: key "', key, '" found'))
|
||||
value <- private$cache$get(key)$value
|
||||
value
|
||||
},
|
||||
|
||||
set = function(key, value) {
|
||||
private$log(paste0('set: key "', key, '"'))
|
||||
validate_key(key)
|
||||
|
||||
time <- as.numeric(Sys.time())
|
||||
|
||||
# Only record size if we're actually using max_size for pruning.
|
||||
if (is.finite(private$max_size)) {
|
||||
# Reported size is rough! See ?object.size.
|
||||
size <- as.numeric(object.size(value))
|
||||
} else {
|
||||
size <- NULL
|
||||
}
|
||||
|
||||
private$cache$set(key, list(
|
||||
key = key,
|
||||
value = value,
|
||||
size = size,
|
||||
mtime = time,
|
||||
atime = time
|
||||
))
|
||||
self$prune()
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
exists = function(key) {
|
||||
validate_key(key)
|
||||
private$cache$has(key)
|
||||
},
|
||||
|
||||
keys = function() {
|
||||
private$cache$keys()
|
||||
},
|
||||
|
||||
remove = function(key) {
|
||||
private$log(paste0('remove: key "', key, '"'))
|
||||
validate_key(key)
|
||||
private$cache$remove(key)
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
reset = function() {
|
||||
private$log(paste0('reset'))
|
||||
private$cache$reset()
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
prune = function() {
|
||||
private$log(paste0('prune'))
|
||||
info <- private$object_info()
|
||||
|
||||
# 1. Remove any objects where the age exceeds max age.
|
||||
if (is.finite(private$max_age)) {
|
||||
time <- as.numeric(Sys.time())
|
||||
timediff <- time - info$mtime
|
||||
rm_idx <- timediff > private$max_age
|
||||
if (any(rm_idx)) {
|
||||
private$log(paste0("prune max_age: Removing ", paste(info$key[rm_idx], collapse = ", ")))
|
||||
private$cache$remove(info$key[rm_idx])
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
}
|
||||
|
||||
# Sort objects by priority, according to eviction policy. The sorting is
|
||||
# done in a function which can be called multiple times but only does
|
||||
# the work the first time.
|
||||
info_is_sorted <- FALSE
|
||||
ensure_info_is_sorted <- function() {
|
||||
if (info_is_sorted) return()
|
||||
|
||||
if (private$evict == "lru") {
|
||||
info <<- info[order(info$atime, decreasing = TRUE), ]
|
||||
} else if (private$evict == "fifo") {
|
||||
info <<- info[order(info$mtime, decreasing = TRUE), ]
|
||||
} else {
|
||||
stop('Unknown eviction policy "', private$evict, '"')
|
||||
}
|
||||
info_is_sorted <<- TRUE
|
||||
}
|
||||
|
||||
# 2. Remove objects if there are too many.
|
||||
if (is.finite(private$max_n) && nrow(info) > private$max_n) {
|
||||
ensure_info_is_sorted()
|
||||
rm_idx <- seq_len(nrow(info)) > private$max_n
|
||||
private$log(paste0("prune max_n: Removing ", paste(info$key[rm_idx], collapse = ", ")))
|
||||
private$cache$remove(info$key[rm_idx])
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
|
||||
# 3. Remove objects if cache is too large.
|
||||
if (is.finite(private$max_size) && sum(info$size) > private$max_size) {
|
||||
ensure_info_is_sorted()
|
||||
cum_size <- cumsum(info$size)
|
||||
rm_idx <- cum_size > private$max_size
|
||||
private$log(paste0("prune max_size: Removing ", paste(info$key[rm_idx], collapse = ", ")))
|
||||
private$cache$remove(info$key[rm_idx])
|
||||
info <- info[!rm_idx, ]
|
||||
}
|
||||
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
size = function() {
|
||||
length(self$keys())
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
cache = NULL,
|
||||
max_age = NULL,
|
||||
max_size = NULL,
|
||||
max_n = NULL,
|
||||
evict = NULL,
|
||||
missing = NULL,
|
||||
exec_missing = NULL,
|
||||
logfile = NULL,
|
||||
|
||||
# Prunes a single object if it exceeds max_age. If the object does not
|
||||
# exceed max_age, or if the object doesn't exist, do nothing.
|
||||
maybe_prune_single = function(key) {
|
||||
if (!is.finite(private$max_age)) return()
|
||||
|
||||
obj <- private$cache$get(key)
|
||||
if (is.null(obj)) return()
|
||||
|
||||
timediff <- as.numeric(Sys.time()) - obj$mtime
|
||||
if (timediff > private$max_age) {
|
||||
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
|
||||
private$cache$remove(key)
|
||||
}
|
||||
},
|
||||
|
||||
object_info = function() {
|
||||
keys <- private$cache$keys()
|
||||
data.frame(
|
||||
key = keys,
|
||||
size = vapply(keys, function(key) private$cache$get(key)$size, 0),
|
||||
mtime = vapply(keys, function(key) private$cache$get(key)$mtime, 0),
|
||||
atime = vapply(keys, function(key) private$cache$get(key)$atime, 0),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
},
|
||||
|
||||
log = function(text) {
|
||||
if (is.null(private$logfile)) return()
|
||||
|
||||
text <- paste0(format(Sys.time(), "[%Y-%m-%d %H:%M:%OS3] MemoryCache "), text)
|
||||
cat(text, sep = "\n", file = private$logfile, append = TRUE)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -1,9 +1,25 @@
|
||||
|
||||
validate_key <- function(key) {
|
||||
if (!is.character(key) || length(key) != 1 || nchar(key) == 0) {
|
||||
stop("Invalid key: key must be single non-empty string.")
|
||||
}
|
||||
if (grepl("[^a-z0-9]", key)) {
|
||||
stop("Invalid key: ", key, ". Only lowercase letters and numbers are allowed.")
|
||||
}
|
||||
# For our purposes, cache objects must support these methods.
|
||||
is_cache_object <- function(x) {
|
||||
# Use tryCatch in case the object does not support `$`.
|
||||
tryCatch(
|
||||
is.function(x$get) && is.function(x$set),
|
||||
error = function(e) FALSE
|
||||
)
|
||||
}
|
||||
|
||||
# Given a cache object, or string "app" or "session", return appropriate cache
|
||||
# object.
|
||||
resolve_cache_object <- function(cache, session) {
|
||||
if (identical(cache, "app")) {
|
||||
cache <- getShinyOption("cache", default = NULL)
|
||||
|
||||
} else if (identical(cache, "session")) {
|
||||
cache <- session$cache
|
||||
}
|
||||
|
||||
if (is_cache_object(cache)) {
|
||||
return(cache)
|
||||
}
|
||||
|
||||
stop('`cache` must either be "app", "session", or a cache object with methods, `$get`, and `$set`.')
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ captureStackTraces <- function(expr) {
|
||||
createStackTracePromiseDomain <- function() {
|
||||
# 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.
|
||||
|
||||
|
||||
d <- promises::new_promise_domain(
|
||||
wrapOnFulfilled = function(onFulfilled) {
|
||||
force(onFulfilled)
|
||||
@@ -217,7 +217,7 @@ doCaptureStack <- function(e) {
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
withLogErrors <- function(expr,
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
full = get_devmode_option("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
withCallingHandlers(
|
||||
@@ -264,34 +264,34 @@ withLogErrors <- function(expr,
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
printError <- function(cond,
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
full = get_devmode_option("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
warning(call. = FALSE, immediate. = TRUE, sprintf("Error in %s: %s",
|
||||
|
||||
warning(call. = FALSE, immediate. = TRUE, sprintf("Error in %s: %s",
|
||||
getCallNames(list(conditionCall(cond))), conditionMessage(cond)))
|
||||
|
||||
|
||||
printStackTrace(cond, full = full, offset = offset)
|
||||
}
|
||||
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
printStackTrace <- function(cond,
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
full = get_devmode_option("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
should_drop <- !full
|
||||
should_strip <- !full
|
||||
should_prune <- !full
|
||||
|
||||
|
||||
stackTraceCalls <- c(
|
||||
attr(cond, "deep.stack.trace", exact = TRUE),
|
||||
list(attr(cond, "stack.trace", exact = TRUE))
|
||||
)
|
||||
|
||||
|
||||
stackTraceParents <- lapply(stackTraceCalls, attr, which = "parents", exact = TRUE)
|
||||
stackTraceCallNames <- lapply(stackTraceCalls, getCallNames)
|
||||
stackTraceCalls <- lapply(stackTraceCalls, offsetSrcrefs, offset = offset)
|
||||
|
||||
|
||||
# Use dropTrivialFrames logic to remove trailing bits (.handleSimpleError, h)
|
||||
if (should_drop) {
|
||||
# toKeep is a list of logical vectors, of which elements (stack frames) to keep
|
||||
@@ -301,7 +301,7 @@ printStackTrace <- function(cond,
|
||||
stackTraceCallNames <- mapply(stackTraceCallNames, FUN = `[`, toKeep, SIMPLIFY = FALSE)
|
||||
stackTraceParents <- mapply(stackTraceParents, FUN = `[`, toKeep, SIMPLIFY = FALSE)
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
@@ -310,7 +310,7 @@ printStackTrace <- function(cond,
|
||||
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.
|
||||
@@ -320,7 +320,7 @@ printStackTrace <- function(cond,
|
||||
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)),
|
||||
@@ -329,7 +329,7 @@ printStackTrace <- function(cond,
|
||||
category = rev(getCallCategories(calls[index])),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
|
||||
|
||||
if (i != 1) {
|
||||
message("From earlier call:")
|
||||
}
|
||||
@@ -357,7 +357,7 @@ printStackTrace <- function(cond,
|
||||
|
||||
st
|
||||
}, SIMPLIFY = FALSE)
|
||||
|
||||
|
||||
invisible()
|
||||
}
|
||||
|
||||
@@ -370,12 +370,13 @@ printStackTrace <- function(cond,
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
extractStackTrace <- function(calls,
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
full = get_devmode_option("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
shinyDeprecated(NULL,
|
||||
"extractStackTrace is deprecated. Please contact the Shiny team if you were using this functionality.",
|
||||
version = "1.0.5")
|
||||
|
||||
shinyDeprecated(
|
||||
"1.0.5", "extractStackTrace()",
|
||||
details = "Please contact the Shiny team if you were using this functionality."
|
||||
)
|
||||
|
||||
srcrefs <- getSrcRefs(calls)
|
||||
if (offset) {
|
||||
@@ -459,19 +460,19 @@ stripOneStackTrace <- function(stackTrace, truncateFloor, startingScore) {
|
||||
prefix <- rep_len(FALSE, indexOfFloor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (length(stackTrace) == 0) {
|
||||
return(list(score = startingScore, character(0)))
|
||||
}
|
||||
|
||||
|
||||
score <- rep.int(0L, length(stackTrace))
|
||||
score[stackTrace == "..stacktraceon.."] <- 1L
|
||||
score[stackTrace == "..stacktraceoff.."] <- -1L
|
||||
score <- startingScore + cumsum(score)
|
||||
|
||||
|
||||
toShow <- score > 0 & !(stackTrace %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
|
||||
|
||||
|
||||
|
||||
|
||||
list(score = utils::tail(score, 1), trace = c(prefix, toShow))
|
||||
}
|
||||
|
||||
@@ -486,11 +487,11 @@ pruneStackTrace <- function(parents) {
|
||||
# sufficient; we also need to drop nodes that are the last child, but one of
|
||||
# their ancestors is not.
|
||||
is_dupe <- duplicated(parents, fromLast = TRUE)
|
||||
|
||||
|
||||
# The index of the most recently seen node that was actually kept instead of
|
||||
# dropped.
|
||||
current_node <- 0
|
||||
|
||||
|
||||
# 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
|
||||
# is kept becomes the new current_node.
|
||||
@@ -502,7 +503,7 @@ pruneStackTrace <- function(parents) {
|
||||
FALSE
|
||||
}
|
||||
}, FUN.VALUE = logical(1))
|
||||
|
||||
|
||||
include
|
||||
}
|
||||
|
||||
@@ -515,7 +516,7 @@ dropTrivialFrames <- function(callnames) {
|
||||
# What's the last that *didn't* match stop/.handleSimpleError/h?
|
||||
lastGoodCall <- max(which(!hideable))
|
||||
toRemove <- length(callnames) - lastGoodCall
|
||||
|
||||
|
||||
c(
|
||||
rep_len(TRUE, length(callnames) - toRemove),
|
||||
rep_len(FALSE, toRemove)
|
||||
@@ -530,10 +531,10 @@ offsetSrcrefs <- function(calls, offset = TRUE) {
|
||||
# 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)
|
||||
}
|
||||
|
||||
|
||||
calls
|
||||
}
|
||||
|
||||
@@ -544,13 +545,14 @@ offsetSrcrefs <- function(calls, offset = TRUE) {
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
formatStackTrace <- function(calls, indent = " ",
|
||||
full = getOption("shiny.fullstacktrace", FALSE),
|
||||
full = get_devmode_option("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
shinyDeprecated(NULL,
|
||||
"extractStackTrace is deprecated. Please contact the Shiny team if you were using this functionality.",
|
||||
version = "1.0.5")
|
||||
|
||||
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))
|
||||
|
||||
108
R/deprecated.R
Normal file
108
R/deprecated.R
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
#' Print message for deprecated functions in Shiny
|
||||
#'
|
||||
#' To disable these messages, use `options(shiny.deprecation.messages=FALSE)`.
|
||||
#'
|
||||
#' @param version Shiny version when the function was deprecated
|
||||
#' @param what Function with possible arguments
|
||||
#' @param with Possible function with arguments that should be used instead
|
||||
#' @param details Additional information to be added after a new line to the displayed message
|
||||
#' @keywords internal
|
||||
shinyDeprecated <- function(
|
||||
version, what, with = NULL, details = NULL
|
||||
) {
|
||||
if (is_false(getOption("shiny.deprecation.messages"))) {
|
||||
return(invisible())
|
||||
}
|
||||
|
||||
msg <- paste0("`", what, "` is deprecated as of shiny ", version, ".")
|
||||
if (!is.null(with)) {
|
||||
msg <- paste0(msg, "\n", "Please use `", with, "` instead.")
|
||||
}
|
||||
if (!is.null(details)) {
|
||||
msg <- paste0(msg, "\n", details)
|
||||
}
|
||||
|
||||
# lifecycle::deprecate_soft(when, what, with = with, details = details, id = id, env = env)
|
||||
rlang::inform(message = msg, .frequency = "always", .frequency_id = msg, .file = stderr())
|
||||
}
|
||||
|
||||
|
||||
deprecatedEnvQuotedMessage <- function() {
|
||||
if (!in_devmode()) return(invisible())
|
||||
if (is_false(getOption("shiny.deprecation.messages"))) return(invisible())
|
||||
|
||||
# manually
|
||||
msg <- paste0(
|
||||
"The `env` and `quoted` arguments are deprecated as of shiny 1.6.0.",
|
||||
" Please use quosures from `rlang` instead.\n",
|
||||
"See <https://github.com/rstudio/shiny/issues/3108> for more information."
|
||||
)
|
||||
rlang::inform(message = msg, .frequency = "always", .frequency_id = msg, .file = stderr())
|
||||
}
|
||||
|
||||
|
||||
#' Create disk cache (deprecated)
|
||||
#'
|
||||
#' @param exec_missing Deprecated.
|
||||
#' @inheritParams cachem::cache_disk
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
diskCache <- function(
|
||||
dir = NULL,
|
||||
max_size = 500 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
destroy_on_finalize = FALSE,
|
||||
missing = key_missing(),
|
||||
exec_missing = deprecated(),
|
||||
logfile = NULL
|
||||
) {
|
||||
shinyDeprecated("1.6.0", "diskCache()", "cachem::cache_disk()")
|
||||
if (lifecycle::is_present(exec_missing)) {
|
||||
shinyDeprecated("1.6.0", "diskCache(exec_missing =)")
|
||||
}
|
||||
|
||||
cachem::cache_disk(
|
||||
dir = dir,
|
||||
max_size = max_size,
|
||||
max_age = max_age,
|
||||
max_n = max_n,
|
||||
evict = evict,
|
||||
destroy_on_finalize = destroy_on_finalize,
|
||||
missing = missing,
|
||||
logfile = logfile
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' Create memory cache (deprecated)
|
||||
#'
|
||||
#' @param exec_missing Deprecated.
|
||||
#' @inheritParams cachem::cache_mem
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
memoryCache <- function(
|
||||
max_size = 200 * 1024 ^ 2,
|
||||
max_age = Inf,
|
||||
max_n = Inf,
|
||||
evict = c("lru", "fifo"),
|
||||
missing = key_missing(),
|
||||
exec_missing = deprecated(),
|
||||
logfile = NULL)
|
||||
{
|
||||
shinyDeprecated("1.6.0", "diskCache()", "cachem::cache_mem()")
|
||||
if (lifecycle::is_present(exec_missing)) {
|
||||
shinyDeprecated("1.6.0", "diskCache(exec_missing =)")
|
||||
}
|
||||
|
||||
cachem::cache_mem(
|
||||
max_size = max_size,
|
||||
max_age = max_age,
|
||||
max_n = max_n,
|
||||
evict = evict,
|
||||
missing = missing,
|
||||
logfile = logfile
|
||||
)
|
||||
}
|
||||
358
R/devmode.R
Normal file
358
R/devmode.R
Normal file
@@ -0,0 +1,358 @@
|
||||
#' Shiny Developer Mode
|
||||
#'
|
||||
#' @description \lifecycle{experimental}
|
||||
#'
|
||||
#' Developer Mode enables a number of [options()] to make a developer's life
|
||||
#' easier, like enabling non-minified JS and printing messages about
|
||||
#' deprecated functions and options.
|
||||
#'
|
||||
#' Shiny Developer Mode can be enabled by calling `devmode(TRUE)` and disabled
|
||||
#' by calling `devmode(FALSE)`.
|
||||
#'
|
||||
#' Please see the function descriptions for more details.
|
||||
#'
|
||||
#' @describeIn devmode Function to set two options to enable/disable Shiny
|
||||
#' Developer Mode and Developer messages
|
||||
#' @param devmode Logical value which should be set to `TRUE` to enable Shiny
|
||||
#' Developer Mode
|
||||
#' @param verbose Logical value which should be set to `TRUE` display Shiny
|
||||
#' Developer messages
|
||||
#' @export
|
||||
#' @examples
|
||||
#' # Enable Shiny Developer mode
|
||||
#' devmode()
|
||||
#'
|
||||
devmode <- function(
|
||||
devmode = getOption("shiny.devmode", TRUE),
|
||||
verbose = getOption("shiny.devmode.verbose", TRUE)
|
||||
) {
|
||||
options(
|
||||
shiny.devmode = devmode,
|
||||
shiny.devmode.verbose = verbose
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' @describeIn devmode Determines if Shiny is in Developer Mode. If the
|
||||
#' `getOption("shiny.devmode")` is set to `TRUE` and not in testing inside
|
||||
#' `testthat`, then Shiny Developer Mode is enabled.
|
||||
#' @section Avoiding direct dependency on shiny:
|
||||
#'
|
||||
#' The methods explained in this help file act independently from the rest of
|
||||
#' Shiny but are included to provide blue prints for your own packages. If
|
||||
#' your package already has (or is willing to take) a dependency on Shiny, we
|
||||
#' recommend using the exported Shiny methods for consistent behavior. Note
|
||||
#' that if you use exported Shiny methods, it will cause the Shiny package to
|
||||
#' load. This may be undesirable if your code will be used in (for example) R
|
||||
#' Markdown documents that do not have a Shiny runtime (`runtime: shiny`).
|
||||
#'
|
||||
#' If your package can **not** take a dependency on Shiny, we recommending
|
||||
#' re-implementing these two functions:
|
||||
#'
|
||||
#' \enumerate{
|
||||
#' \item `in_devmode()`:
|
||||
#'
|
||||
#' This function should return `TRUE` if `getOption("shiny.devmode")` is set.
|
||||
#' In addition, we strongly recommend that it also checks to make sure
|
||||
#' `testthat` is not testing.
|
||||
#'
|
||||
#' ```r
|
||||
#' in_devmode <- function() {
|
||||
#' isTRUE(getOption("shiny.devmode", FALSE)) &&
|
||||
#' !identical(Sys.getenv("TESTTHAT"), "true")
|
||||
#' }
|
||||
#' ```
|
||||
#'
|
||||
#' \item `get_devmode_option(name, default, devmode_default, devmode_message)`:
|
||||
#'
|
||||
#' This function is similar to `getOption(name, default)`, but when the option
|
||||
#' is not set, the default value changes depending on the Dev Mode.
|
||||
#' `get_devmode_option()` should be implemented as follows:
|
||||
#'
|
||||
#' * If not in Dev Mode:
|
||||
#' * Return `getOption(name, default)`.
|
||||
#' * If in Dev Mode:
|
||||
#' * Get the global option `getOption(name)` value.
|
||||
#' * If the global option value is set:
|
||||
#' * Return the value.
|
||||
#' * If the global option value is not set:
|
||||
#' * Notify the developer that the Dev Mode default value will be used.
|
||||
#' * Return the Dev Mode default value.
|
||||
#'
|
||||
#' When notifying the developer that the default value has changed, we strongly
|
||||
#' recommend displaying a message (`devmode_message`) to `stderr()` once every 8
|
||||
#' hours using [rlang::inform()]. This will keep the author up to date as to
|
||||
#' which Dev Mode options are being altered. To allow developers a chance to
|
||||
#' disable Dev Mode messages, the message should be skipped if
|
||||
#' `getOption("shiny.devmode.verbose", TRUE)` is not `TRUE`.
|
||||
#'
|
||||
#' ```r
|
||||
#' get_devmode_option <- function(name, default = NULL, devmode_default, devmode_message) {
|
||||
#' if (!in_devmode()) {
|
||||
#' # Dev Mode disabled, act like `getOption()`
|
||||
#' return(getOption(name, default = default))
|
||||
#' }
|
||||
#'
|
||||
#' # Dev Mode enabled, update the default value for `getOption()`
|
||||
#' getOption(name, default = {
|
||||
#' # Notify developer
|
||||
#' if (
|
||||
#' !missing(devmode_message) &&
|
||||
#' !is.null(devmode_message) &&
|
||||
#' getOption("shiny.devmode.verbose", TRUE)
|
||||
#' ) {
|
||||
#' rlang::inform(
|
||||
#' message = devmode_message,
|
||||
#' .frequency = "regularly",
|
||||
#' .frequency_id = devmode_message,
|
||||
#' .file = stderr()
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' # Return Dev Mode default value `devmode_default`
|
||||
#' devmode_default
|
||||
#' })
|
||||
#' }
|
||||
#' ```
|
||||
#' }
|
||||
#'
|
||||
#' The remaining functions in this file are used for author convenience and are
|
||||
#' not recommended for all reimplementation situations.
|
||||
#' @export
|
||||
#' @examples
|
||||
#' in_devmode() # TRUE/FALSE?
|
||||
#'
|
||||
in_devmode <- function() {
|
||||
isTRUE(getOption("shiny.devmode", FALSE)) &&
|
||||
# !testthat::is_testing()
|
||||
!identical(Sys.getenv("TESTTHAT"), "true")
|
||||
}
|
||||
|
||||
#' @describeIn devmode Temporarily set Shiny Developer Mode and Developer
|
||||
#' message verbosity
|
||||
#' @param code Code to execute with the temporary Dev Mode options set
|
||||
#' @export
|
||||
#' @examples
|
||||
#' # Execute code in a temporary shiny dev mode
|
||||
#' with_devmode(TRUE, in_devmode()) # TRUE
|
||||
#'
|
||||
with_devmode <- function(
|
||||
devmode,
|
||||
code,
|
||||
verbose = getOption("shiny.devmode.verbose", TRUE)
|
||||
) {
|
||||
withr::with_options(
|
||||
list(
|
||||
shiny.devmode = devmode,
|
||||
shiny.devmode.verbose = verbose
|
||||
),
|
||||
code
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' @describeIn devmode If Shiny Developer Mode and verbosity are enabled,
|
||||
#' displays a message once every 8 hrs (by default)
|
||||
#' @param message Developer Mode message to be sent to [rlang::inform()]
|
||||
#' @param .frequency Frequency of the Developer Mode message used with
|
||||
#' [rlang::inform()]. Defaults to once every 8 hours.
|
||||
#' @param .frequency_id [rlang::inform()] message identifier. Defaults to
|
||||
#' `message`.
|
||||
#' @param .file Output connection for [rlang::inform()]. Defaults to [stderr()]
|
||||
#' @param ... Parameters passed to [rlang::inform()]
|
||||
devmode_inform <- function(
|
||||
message,
|
||||
.frequency = "regularly",
|
||||
.frequency_id = message,
|
||||
.file = stderr(),
|
||||
...
|
||||
) {
|
||||
|
||||
if (!(
|
||||
in_devmode() &&
|
||||
isTRUE(getOption("shiny.devmode.verbose", TRUE))
|
||||
)) {
|
||||
return()
|
||||
}
|
||||
if (is.null(message)) {
|
||||
return()
|
||||
}
|
||||
|
||||
rlang::inform(
|
||||
message = paste0("shiny devmode - ", message),
|
||||
.frequency = .frequency,
|
||||
.frequency_id = .frequency_id,
|
||||
.file = .file,
|
||||
...
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#' @include map.R
|
||||
registered_devmode_options <- Map$new()
|
||||
|
||||
#' @describeIn devmode Registers a Shiny Developer Mode option with an updated
|
||||
#' value and Developer message. This registration method allows package
|
||||
#' authors to write one message in a single location.
|
||||
#'
|
||||
#' For example, the following Shiny Developer Mode options are registered:
|
||||
#'
|
||||
#' ```r
|
||||
#' # Reload the Shiny app when a sourced R file changes
|
||||
#' register_devmode_option(
|
||||
#' "shiny.autoreload",
|
||||
#' "Turning on shiny autoreload. To disable, call `options(shiny.autoreload = FALSE)`",
|
||||
#' devmode_default = TRUE
|
||||
#' )
|
||||
#'
|
||||
#' # Use the unminified Shiny JavaScript file, `shiny.js`
|
||||
#' register_devmode_option(
|
||||
#' "shiny.minified",
|
||||
#' "Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`",
|
||||
#' devmode_default = FALSE
|
||||
#' )
|
||||
#'
|
||||
#' # Display the full stack trace when errors occur during Shiny app execution
|
||||
#' register_devmode_option(
|
||||
#' "shiny.fullstacktrace",
|
||||
#' "Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`",
|
||||
#' devmode_default = TRUE
|
||||
#' )
|
||||
#' ```
|
||||
#'
|
||||
#' Other known, non-Shiny Developer Mode options:
|
||||
#'
|
||||
#' * Sass:
|
||||
#' ```r
|
||||
#' # Display the full stack trace when errors occur during Shiny app execution
|
||||
#' register_devmode_option(
|
||||
#' "sass.cache",
|
||||
#' "Turning off sass cache. To use default caching, call `options(sass.cache = TRUE)`",
|
||||
#' devmode_default = FALSE
|
||||
#' )
|
||||
#' ```
|
||||
#' @param name Name of option to look for in `options()`
|
||||
#' @param default Default value to return if `in_devmode()` returns
|
||||
#' `TRUE` and the specified option is not set in [`options()`].
|
||||
#' @param devmode_message Message to display once every 8 hours when utilizing
|
||||
#' the `devmode_default` value. If `devmode_message` is missing, the
|
||||
#' registered `devmode_message` value be used.
|
||||
#' @param devmode_default Default value to return if `in_devmode()` returns
|
||||
#' `TRUE` and the specified option is not set in [`options()`]. For
|
||||
#' `get_devmode_option()`, if `devmode_default` is missing, the
|
||||
#' registered `devmode_default` value will be used.
|
||||
#' @examples
|
||||
#' # Ex: Within shiny, we register the option "shiny.minified"
|
||||
#' # to default to `FALSE` when in Dev Mode
|
||||
#' \dontrun{register_devmode_option(
|
||||
#' "shiny.minified",
|
||||
#' devmode_message = paste0(
|
||||
#' "Using full shiny javascript file. ",
|
||||
#' "To use the minified version, call `options(shiny.minified = TRUE)`"
|
||||
#' ),
|
||||
#' devmode_default = FALSE
|
||||
#' )}
|
||||
#'
|
||||
register_devmode_option <- function(
|
||||
name,
|
||||
devmode_message = NULL,
|
||||
devmode_default = NULL
|
||||
) {
|
||||
if (!is.null(devmode_message)) {
|
||||
stopifnot(length(devmode_message) == 1 && is.character(devmode_message))
|
||||
}
|
||||
registered_devmode_options$set(
|
||||
name,
|
||||
list(devmode_default = devmode_default, devmode_message = devmode_message)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' @describeIn devmode Provides a consistent way to change the expected
|
||||
#' [getOption()] behavior when Developer Mode is enabled. This method is very
|
||||
#' similar to [getOption()] where the globally set option takes precedence.
|
||||
#' See section "Avoiding direct dependency on shiny" for
|
||||
#' `get_devmode_option()` implementation details.
|
||||
#'
|
||||
#' **Package developers:** Register your Dev Mode option using
|
||||
#' `register_devmode_option()` to avoid supplying the same `devmode_default`
|
||||
#' and `devmode_message` values throughout your package. (This requires a
|
||||
#' \pkg{shiny} dependency.)
|
||||
#' @export
|
||||
#' @examples
|
||||
#' # Used within `shiny::runApp(launch.browser)`
|
||||
#' get_devmode_option("shiny.minified", TRUE) # TRUE if Dev mode is off
|
||||
#' is_minified <- with_devmode(TRUE, {
|
||||
#' get_devmode_option("shiny.minified", TRUE)
|
||||
#' })
|
||||
#' is_minified # FALSE
|
||||
#'
|
||||
get_devmode_option <- function(
|
||||
name,
|
||||
default = NULL,
|
||||
devmode_default = missing_arg(),
|
||||
devmode_message = missing_arg()
|
||||
) {
|
||||
getOption(
|
||||
name,
|
||||
local({
|
||||
if (!in_devmode()) {
|
||||
# typical case
|
||||
return(default)
|
||||
}
|
||||
|
||||
info <- registered_devmode_options$get(name)
|
||||
if (is.null(info)) {
|
||||
# Not registered,
|
||||
# Warn and return default value
|
||||
rlang::warn(
|
||||
message = paste0(
|
||||
"`get_devmode_option(name)` could not find `name = \"", name, "\"`. ",
|
||||
"Returning `default` value"
|
||||
)
|
||||
)
|
||||
return(default)
|
||||
}
|
||||
|
||||
# display message
|
||||
devmode_inform(
|
||||
maybe_missing(
|
||||
# use provided `devmode_message` value
|
||||
devmode_message,
|
||||
# If `devmode_message` is missing, display registered `devmode_message`
|
||||
default = info$devmode_message
|
||||
)
|
||||
)
|
||||
|
||||
# return value
|
||||
maybe_missing(
|
||||
# use provided `devmode_default` value
|
||||
devmode_default,
|
||||
# if `devmode_default` is missing, provide registered `devmode_default`
|
||||
default = info$devmode_default
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
register_devmode_option(
|
||||
"shiny.autoreload",
|
||||
"Turning on shiny autoreload. To disable, call `options(shiny.autoreload = FALSE)`",
|
||||
TRUE
|
||||
)
|
||||
|
||||
register_devmode_option(
|
||||
"shiny.minified",
|
||||
"Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`",
|
||||
FALSE
|
||||
)
|
||||
|
||||
register_devmode_option(
|
||||
"shiny.fullstacktrace",
|
||||
"Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`",
|
||||
TRUE
|
||||
)
|
||||
@@ -1,10 +1,10 @@
|
||||
# Generated by tools/updateFontAwesome.R: do not edit by hand
|
||||
font_awesome_brands <- c(
|
||||
"500px",
|
||||
"accessible-icon",
|
||||
"accusoft",
|
||||
"acquisitions-incorporated",
|
||||
"adn",
|
||||
"adobe",
|
||||
"adversal",
|
||||
"affiliatetheme",
|
||||
"airbnb",
|
||||
@@ -65,6 +65,7 @@ font_awesome_brands <- c(
|
||||
"centos",
|
||||
"chrome",
|
||||
"chromecast",
|
||||
"cloudflare",
|
||||
"cloudscale",
|
||||
"cloudsmith",
|
||||
"cloudversify",
|
||||
@@ -97,6 +98,7 @@ font_awesome_brands <- c(
|
||||
"d-and-d-beyond",
|
||||
"dailymotion",
|
||||
"dashcube",
|
||||
"deezer",
|
||||
"delicious",
|
||||
"deploydog",
|
||||
"deskpro",
|
||||
@@ -119,6 +121,7 @@ font_awesome_brands <- c(
|
||||
"earlybirds",
|
||||
"ebay",
|
||||
"edge",
|
||||
"edge-legacy",
|
||||
"elementor",
|
||||
"ello",
|
||||
"ember",
|
||||
@@ -179,6 +182,7 @@ font_awesome_brands <- c(
|
||||
"goodreads-g",
|
||||
"google",
|
||||
"google-drive",
|
||||
"google-pay",
|
||||
"google-play",
|
||||
"google-plus",
|
||||
"google-plus-g",
|
||||
@@ -188,12 +192,14 @@ font_awesome_brands <- c(
|
||||
"grav",
|
||||
"gripfire",
|
||||
"grunt",
|
||||
"guilded",
|
||||
"gulp",
|
||||
"hacker-news",
|
||||
"hacker-news-square",
|
||||
"hackerrank",
|
||||
"hips",
|
||||
"hire-a-helper",
|
||||
"hive",
|
||||
"hooli",
|
||||
"hornbill",
|
||||
"hotjar",
|
||||
@@ -202,8 +208,10 @@ font_awesome_brands <- c(
|
||||
"hubspot",
|
||||
"ideal",
|
||||
"imdb",
|
||||
"innosoft",
|
||||
"instagram",
|
||||
"instagram-square",
|
||||
"instalod",
|
||||
"intercom",
|
||||
"internet-explorer",
|
||||
"invision",
|
||||
@@ -267,6 +275,7 @@ font_awesome_brands <- c(
|
||||
"npm",
|
||||
"ns8",
|
||||
"nutritionix",
|
||||
"octopus-deploy",
|
||||
"odnoklassniki",
|
||||
"odnoklassniki-square",
|
||||
"old-republic",
|
||||
@@ -282,6 +291,7 @@ font_awesome_brands <- c(
|
||||
"patreon",
|
||||
"paypal",
|
||||
"penny-arcade",
|
||||
"perbyte",
|
||||
"periscope",
|
||||
"phabricator",
|
||||
"phoenix-framework",
|
||||
@@ -321,6 +331,7 @@ font_awesome_brands <- c(
|
||||
"rev",
|
||||
"rocketchat",
|
||||
"rockrms",
|
||||
"rust",
|
||||
"safari",
|
||||
"salesforce",
|
||||
"sass",
|
||||
@@ -378,6 +389,7 @@ font_awesome_brands <- c(
|
||||
"themeco",
|
||||
"themeisle",
|
||||
"think-peaks",
|
||||
"tiktok",
|
||||
"trade-federation",
|
||||
"trello",
|
||||
"tripadvisor",
|
||||
@@ -391,8 +403,10 @@ font_awesome_brands <- c(
|
||||
"ubuntu",
|
||||
"uikit",
|
||||
"umbraco",
|
||||
"uncharted",
|
||||
"uniregistry",
|
||||
"unity",
|
||||
"unsplash",
|
||||
"untappd",
|
||||
"ups",
|
||||
"usb",
|
||||
@@ -410,6 +424,7 @@ font_awesome_brands <- c(
|
||||
"vk",
|
||||
"vnv",
|
||||
"vuejs",
|
||||
"watchman-monitoring",
|
||||
"waze",
|
||||
"weebly",
|
||||
"weibo",
|
||||
@@ -421,6 +436,7 @@ font_awesome_brands <- c(
|
||||
"windows",
|
||||
"wix",
|
||||
"wizards-of-the-coast",
|
||||
"wodu",
|
||||
"wolf-pack-battalion",
|
||||
"wordpress",
|
||||
"wordpress-simple",
|
||||
|
||||
@@ -56,6 +56,10 @@ register_upgrade_message <- function(pkg, version) {
|
||||
# the private seed during load.
|
||||
withPrivateSeed(set.seed(NULL))
|
||||
|
||||
# Create this at the top level, but since the object is from a different
|
||||
# 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
|
||||
# attached.
|
||||
register_s3_method("knitr", "knit_print", "reactive")
|
||||
|
||||
11
R/graph.R
11
R/graph.R
@@ -10,7 +10,7 @@ check_suggested <- function(package, version = NULL) {
|
||||
|
||||
msg <- paste0(
|
||||
sQuote(package),
|
||||
if (is.na(version %OR% NA)) "" else paste0("(>= ", version, ")"),
|
||||
if (is.na(version %||% NA)) "" else paste0("(>= ", version, ")"),
|
||||
" must be installed for this functionality."
|
||||
)
|
||||
|
||||
@@ -87,7 +87,8 @@ reactlog <- function() {
|
||||
}
|
||||
|
||||
#' @describeIn reactlog Display a full reactlog graph for all sessions.
|
||||
#' @inheritParams reactlog::reactlog_show
|
||||
#' @param time A boolean that specifies whether or not to display the
|
||||
#' time that each reactive takes to calculate a result.
|
||||
#' @export
|
||||
reactlogShow <- function(time = TRUE) {
|
||||
check_reactlog()
|
||||
@@ -97,7 +98,8 @@ reactlogShow <- function(time = TRUE) {
|
||||
#' @export
|
||||
# legacy purposes
|
||||
showReactLog <- function(time = TRUE) {
|
||||
shinyDeprecated(new = "`reactlogShow`", version = "1.2.0")
|
||||
shinyDeprecated("1.2.0", "showReactLog()", "reactlogShow()")
|
||||
|
||||
reactlogShow(time = time)
|
||||
}
|
||||
#' @describeIn reactlog Resets the entire reactlog stack. Useful for debugging and removing all prior reactive history.
|
||||
@@ -210,7 +212,7 @@ RLog <- R6Class(
|
||||
reset = function() {
|
||||
.globals$reactIdCounter <- 0L
|
||||
|
||||
self$logStack <- Stack$new()
|
||||
self$logStack <- fastmap::faststack()
|
||||
self$msg <- MessageLogger$new(option = private$msgOption)
|
||||
|
||||
# setup dummy and missing react information
|
||||
@@ -558,5 +560,4 @@ MessageLogger = R6Class(
|
||||
)
|
||||
)
|
||||
|
||||
#' @include stack.R
|
||||
rLog <- RLog$new("shiny.reactlog", "shiny.reactlog.console")
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#' @import htmltools
|
||||
#' @export tags p h1 h2 h3 h4 h5 h6 a br div span pre code img strong em hr
|
||||
#' @export tag tagList tagAppendAttributes tagHasAttribute tagGetAttribute tagAppendChild tagAppendChildren tagSetChildren
|
||||
#' @export HTML
|
||||
#' @export includeHTML includeText includeMarkdown includeCSS includeScript
|
||||
#' @export singleton is.singleton
|
||||
#' @export validateCssUnit
|
||||
#' @export htmlTemplate
|
||||
#' @export suppressDependencies
|
||||
#' @export withTags
|
||||
NULL
|
||||
@@ -1,15 +1,26 @@
|
||||
#' Create an object representing click options
|
||||
#' Control interactive plot point events
|
||||
#'
|
||||
#' This generates an object representing click options, to be passed as the
|
||||
#' `click` argument of [imageOutput()] or
|
||||
#' [plotOutput()].
|
||||
#' These functions give control over the `click`, `dblClick` and
|
||||
#' `hover` events generated by [imageOutput()] and [plotOutput()].
|
||||
#'
|
||||
#' @param id Input value name. For example, if the value is `"plot_click"`,
|
||||
#' then the click coordinates will be available as `input$plot_click`.
|
||||
#' @param clip Should the click area be clipped to the plotting area? If FALSE,
|
||||
#' then the server will receive click events even when the mouse is outside
|
||||
#' the plotting area, as long as it is still inside the image.
|
||||
#' then the event data will be available as `input$plot_click`.
|
||||
#' @param clip Should the click area be clipped to the plotting area? If
|
||||
#' `FALSE`, then the server will receive click events even when the mouse is
|
||||
#' outside the plotting area, as long as it is still inside the image.
|
||||
#' @param delay For `dblClickOpts()`: the maximum delay (in ms) between a
|
||||
#' pair clicks for them to be counted as a double-click.
|
||||
#'
|
||||
#' For `hoverOpts()`: how long to delay (in ms) when debouncing or throttling
|
||||
#' before sending the mouse location to the server.
|
||||
#' @param delayType The type of algorithm for limiting the number of hover
|
||||
#' events. Use `"throttle"` to limit the number of hover events to one
|
||||
#' every `delay` milliseconds. Use `"debounce"` to suspend events
|
||||
#' while the cursor is moving, and wait until the cursor has been at rest for
|
||||
#' `delay` milliseconds before sending an event.
|
||||
#' @seealso [brushOpts()] for brushing events.
|
||||
#' @export
|
||||
#' @keywords internal
|
||||
clickOpts <- function(id, clip = TRUE) {
|
||||
if (is.null(id))
|
||||
stop("id must not be NULL")
|
||||
@@ -21,21 +32,8 @@ clickOpts <- function(id, clip = TRUE) {
|
||||
}
|
||||
|
||||
|
||||
#' Create an object representing double-click options
|
||||
#'
|
||||
#' This generates an object representing dobule-click options, to be passed as
|
||||
#' the `dblclick` argument of [imageOutput()] or
|
||||
#' [plotOutput()].
|
||||
#'
|
||||
#' @param id Input value name. For example, if the value is
|
||||
#' `"plot_dblclick"`, then the click coordinates will be available as
|
||||
#' `input$plot_dblclick`.
|
||||
#' @param clip Should the click area be clipped to the plotting area? If FALSE,
|
||||
#' then the server will receive double-click events even when the mouse is
|
||||
#' outside the plotting area, as long as it is still inside the image.
|
||||
#' @param delay Maximum delay (in ms) between a pair clicks for them to be
|
||||
#' counted as a double-click.
|
||||
#' @export
|
||||
#' @rdname clickOpts
|
||||
dblclickOpts <- function(id, clip = TRUE, delay = 400) {
|
||||
if (is.null(id))
|
||||
stop("id must not be NULL")
|
||||
@@ -47,28 +45,11 @@ dblclickOpts <- function(id, clip = TRUE, delay = 400) {
|
||||
)
|
||||
}
|
||||
|
||||
#' Create an object representing hover options
|
||||
#'
|
||||
#' This generates an object representing hovering options, to be passed as the
|
||||
#' `hover` argument of [imageOutput()] or
|
||||
#' [plotOutput()].
|
||||
#'
|
||||
#' @param id Input value name. For example, if the value is `"plot_hover"`,
|
||||
#' then the hover coordinates will be available as `input$plot_hover`.
|
||||
#' @param delay How long to delay (in milliseconds) when debouncing or
|
||||
#' throttling, before sending the mouse location to the server.
|
||||
#' @param delayType The type of algorithm for limiting the number of hover
|
||||
#' events. Use `"throttle"` to limit the number of hover events to one
|
||||
#' every `delay` milliseconds. Use `"debounce"` to suspend events
|
||||
#' while the cursor is moving, and wait until the cursor has been at rest for
|
||||
#' `delay` milliseconds before sending an event.
|
||||
#' @param clip Should the hover area be clipped to the plotting area? If FALSE,
|
||||
#' then the server will receive hover events even when the mouse is outside
|
||||
#' the plotting area, as long as it is still inside the image.
|
||||
#' @param nullOutside If `TRUE` (the default), the value will be set to
|
||||
#' `NULL` when the mouse exits the plotting area. If `FALSE`, the
|
||||
#' value will stop changing when the cursor exits the plotting area.
|
||||
#' @export
|
||||
#' @rdname clickOpts
|
||||
hoverOpts <- function(id, delay = 300,
|
||||
delayType = c("debounce", "throttle"), clip = TRUE,
|
||||
nullOutside = TRUE) {
|
||||
@@ -95,8 +76,12 @@ hoverOpts <- function(id, delay = 300,
|
||||
#' `imageOutput`/`plotOutput` calls may share the same `id`
|
||||
#' value; brushing one image or plot will cause any other brushes with the
|
||||
#' same `id` to disappear.
|
||||
#' @param fill Fill color of the brush.
|
||||
#' @param stroke Outline color of the brush.
|
||||
#' @param fill Fill color of the brush. If `'auto'`, it derives from the link
|
||||
#' color of the plot's HTML container (if **thematic** is enabled, and `accent`
|
||||
#' is a non-`'auto'` value, that color is used instead).
|
||||
#' @param stroke Outline color of the brush. If `'auto'`, it derives from the
|
||||
#' foreground color of the plot's HTML container (if **thematic** is enabled,
|
||||
#' and `fg` is a non-`'auto'` value, that color is used instead).
|
||||
#' @param opacity Opacity of the brush
|
||||
#' @param delay How long to delay (in milliseconds) when debouncing or
|
||||
#' throttling, before sending the brush data to the server.
|
||||
@@ -116,6 +101,7 @@ hoverOpts <- function(id, delay = 300,
|
||||
#' `FALSE`, is useful if you want to update the plot while keeping the
|
||||
#' brush. Using `TRUE` is useful if you want to clear the brush whenever
|
||||
#' the plot is updated.
|
||||
#' @seealso [clickOpts()] for clicking events.
|
||||
#' @export
|
||||
brushOpts <- function(id, fill = "#9cf", stroke = "#036",
|
||||
opacity = 0.25, delay = 300,
|
||||
@@ -125,6 +111,13 @@ brushOpts <- function(id, fill = "#9cf", stroke = "#036",
|
||||
if (is.null(id))
|
||||
stop("id must not be NULL")
|
||||
|
||||
if (identical(fill, "auto")) {
|
||||
fill <- getThematicOption("accent", "auto")
|
||||
}
|
||||
if (identical(stroke, "auto")) {
|
||||
stroke <- getThematicOption("fg", "auto")
|
||||
}
|
||||
|
||||
list(
|
||||
id = id,
|
||||
fill = fill,
|
||||
@@ -137,3 +130,13 @@ brushOpts <- function(id, fill = "#9cf", stroke = "#036",
|
||||
resetOnNew = resetOnNew
|
||||
)
|
||||
}
|
||||
|
||||
getThematicOption <- function(name = "", default = NULL, resolve = FALSE) {
|
||||
if (isNamespaceLoaded("thematic")) {
|
||||
# TODO: use :: once thematic is on CRAN
|
||||
tgo <- utils::getFromNamespace("thematic_get_option", "thematic")
|
||||
tgo(name = name, default = default, resolve = resolve)
|
||||
} else {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +92,21 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
|
||||
use_x <- grepl("x", brush$direction)
|
||||
use_y <- grepl("y", brush$direction)
|
||||
|
||||
# We transitioned to using %||% in Shiny 1.6.0. Previously, these vars could
|
||||
# be NA, because the old %OR% operator recognized NA. These warnings and
|
||||
# the NULL replacement are here just to ease the transition in case anyone is
|
||||
# using NA. We can remove these checks in a future version of Shiny.
|
||||
# https://github.com/rstudio/shiny/pull/3172
|
||||
if (is_na(xvar)) { xvar <- NULL; warning("xvar should be NULL, not NA.") }
|
||||
if (is_na(yvar)) { yvar <- NULL; warning("yvar should be NULL, not NA.") }
|
||||
if (is_na(panelvar1)) { panelvar1 <- NULL; warning("panelvar1 should be NULL, not NA.") }
|
||||
if (is_na(panelvar2)) { panelvar2 <- NULL; warning("panelvar2 should be NULL, not NA.") }
|
||||
|
||||
# Try to extract vars from brush object
|
||||
xvar <- xvar %OR% brush$mapping$x
|
||||
yvar <- yvar %OR% brush$mapping$y
|
||||
panelvar1 <- panelvar1 %OR% brush$mapping$panelvar1
|
||||
panelvar2 <- panelvar2 %OR% brush$mapping$panelvar2
|
||||
xvar <- xvar %||% brush$mapping$x
|
||||
yvar <- yvar %||% brush$mapping$y
|
||||
panelvar1 <- panelvar1 %||% brush$mapping$panelvar1
|
||||
panelvar2 <- panelvar2 %||% brush$mapping$panelvar2
|
||||
|
||||
# Filter out x and y values
|
||||
keep_rows <- rep(TRUE, nrow(df))
|
||||
@@ -230,11 +240,21 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
|
||||
stop("nearPoints requires a click/hover/double-click object with x and y values.")
|
||||
}
|
||||
|
||||
# We transitioned to using %||% in Shiny 1.6.0. Previously, these vars could
|
||||
# be NA, because the old %OR% operator recognized NA. These warnings and
|
||||
# the NULL replacement are here just to ease the transition in case anyone is
|
||||
# using NA. We can remove these checks in a future version of Shiny.
|
||||
# https://github.com/rstudio/shiny/pull/3172
|
||||
if (is_na(xvar)) { xvar <- NULL; warning("xvar should be NULL, not NA.") }
|
||||
if (is_na(yvar)) { yvar <- NULL; warning("yvar should be NULL, not NA.") }
|
||||
if (is_na(panelvar1)) { panelvar1 <- NULL; warning("panelvar1 should be NULL, not NA.") }
|
||||
if (is_na(panelvar2)) { panelvar2 <- NULL; warning("panelvar2 should be NULL, not NA.") }
|
||||
|
||||
# Try to extract vars from coordinfo object
|
||||
xvar <- xvar %OR% coordinfo$mapping$x
|
||||
yvar <- yvar %OR% coordinfo$mapping$y
|
||||
panelvar1 <- panelvar1 %OR% coordinfo$mapping$panelvar1
|
||||
panelvar2 <- panelvar2 %OR% coordinfo$mapping$panelvar2
|
||||
xvar <- xvar %||% coordinfo$mapping$x
|
||||
yvar <- yvar %||% coordinfo$mapping$y
|
||||
panelvar1 <- panelvar1 %||% coordinfo$mapping$panelvar1
|
||||
panelvar2 <- panelvar2 %||% coordinfo$mapping$panelvar2
|
||||
|
||||
if (is.null(xvar))
|
||||
stop("nearPoints: not able to automatically infer `xvar` from coordinfo")
|
||||
|
||||
@@ -4,12 +4,12 @@ startPNG <- function(filename, width, height, res, ...) {
|
||||
# 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') %OR% FALSE) && is_available("ragg")) {
|
||||
if ((getOption('shiny.useragg') %||% FALSE) && is_available("ragg")) {
|
||||
pngfun <- ragg::agg_png
|
||||
} else if (capabilities("aqua")) {
|
||||
# i.e., png(type = 'quartz')
|
||||
pngfun <- grDevices::png
|
||||
} else if ((getOption('shiny.usecairo') %OR% TRUE) && is_available("Cairo")) {
|
||||
} else if ((getOption('shiny.usecairo') %||% TRUE) && is_available("Cairo")) {
|
||||
pngfun <- Cairo::CairoPNG
|
||||
} else {
|
||||
# i.e., png(type = 'cairo')
|
||||
@@ -23,8 +23,7 @@ startPNG <- function(filename, width, height, res, ...) {
|
||||
# devices allow their bg arg to be overridden by par(bg=...), which thematic does prior
|
||||
# to plot-time, but it shouldn't hurt to inform other the device directly as well
|
||||
if (is.null(args$bg) && isNamespaceLoaded("thematic")) {
|
||||
# TODO: use :: once thematic is on CRAN
|
||||
args$bg <- utils::getFromNamespace("thematic_get_option", "thematic")("bg", "white")
|
||||
args$bg <- getThematicOption("bg", "white")
|
||||
# auto vals aren't resolved until plot time, so if we see one, resolve it
|
||||
if (isTRUE("auto" == args$bg)) {
|
||||
args$bg <- getCurrentOutputInfo()[["bg"]]()
|
||||
@@ -95,7 +94,6 @@ plotPNG <- function(func, filename=tempfile(fileext='.png'),
|
||||
filename
|
||||
}
|
||||
|
||||
#' @importFrom grDevices dev.set dev.cur
|
||||
createGraphicsDevicePromiseDomain <- function(which = dev.cur()) {
|
||||
force(which)
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
|
||||
value <- restoreInput(id = inputId, default = NULL)
|
||||
|
||||
tags$button(id=inputId,
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
type="button",
|
||||
class="btn btn-default action-button",
|
||||
`data-val` = value,
|
||||
|
||||
@@ -36,7 +36,7 @@ checkboxInput <- function(inputId, label, value = FALSE, width = NULL) {
|
||||
inputTag$attribs$checked <- "checked"
|
||||
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
div(class = "checkbox",
|
||||
tags$label(inputTag, tags$span(label))
|
||||
)
|
||||
|
||||
@@ -94,10 +94,14 @@ checkboxGroupInput <- function(inputId, label, choices = NULL, selected = NULL,
|
||||
divClass <- paste(divClass, "shiny-input-container-inline")
|
||||
|
||||
# return label and select tag
|
||||
inputLabel <- shinyInputLabel(inputId, label)
|
||||
tags$div(id = inputId,
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
class = divClass,
|
||||
shinyInputLabel(inputId, label),
|
||||
# https://www.w3.org/TR/wai-aria-practices/examples/checkbox/checkbox-1/checkbox-1.html
|
||||
role = "group",
|
||||
`aria-labelledby` = inputLabel$attribs$id,
|
||||
inputLabel,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
|
||||
|
||||
tags$div(id = inputId,
|
||||
class = "shiny-date-input form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$input(type = "text",
|
||||
@@ -128,19 +128,49 @@ dateInput <- function(inputId, label, value = NULL, min = NULL, max = NULL,
|
||||
`data-date-days-of-week-disabled` =
|
||||
jsonlite::toJSON(daysofweekdisabled, null = 'null')
|
||||
),
|
||||
datePickerDependency
|
||||
datePickerDependency()
|
||||
)
|
||||
}
|
||||
|
||||
datePickerDependency <- htmlDependency(
|
||||
"bootstrap-datepicker", "1.6.4", c(href = "shared/datepicker"),
|
||||
script = "js/bootstrap-datepicker.min.js",
|
||||
stylesheet = "css/bootstrap-datepicker3.min.css",
|
||||
# Need to enable noConflict mode. See #1346.
|
||||
head = "<script>
|
||||
(function() {
|
||||
var datepicker = $.fn.datepicker.noConflict();
|
||||
$.fn.bsDatepicker = datepicker;
|
||||
})();
|
||||
</script>"
|
||||
)
|
||||
|
||||
datePickerVersion <- "1.9.0"
|
||||
|
||||
datePickerDependency <- function(theme) {
|
||||
list(
|
||||
htmlDependency(
|
||||
name = "bootstrap-datepicker-js",
|
||||
version = datePickerVersion,
|
||||
src = c(href = "shared/datepicker"),
|
||||
script = if (getOption("shiny.minified", TRUE)) "js/bootstrap-datepicker.min.js"
|
||||
else "js/bootstrap-datepicker.js",
|
||||
# Need to enable noConflict mode. See #1346.
|
||||
head = "<script>(function() {
|
||||
var datepicker = $.fn.datepicker.noConflict();
|
||||
$.fn.bsDatepicker = datepicker;
|
||||
})();
|
||||
</script>"
|
||||
),
|
||||
bslib::bs_dependency_defer(datePickerCSS)
|
||||
)
|
||||
}
|
||||
|
||||
datePickerCSS <- function(theme) {
|
||||
if (!is_bs_theme(theme)) {
|
||||
return(htmlDependency(
|
||||
name = "bootstrap-datepicker-css",
|
||||
version = datePickerVersion,
|
||||
src = c(href = "shared/datepicker"),
|
||||
stylesheet = "css/bootstrap-datepicker3.min.css"
|
||||
))
|
||||
}
|
||||
|
||||
scss_file <- system.file(package = "shiny", "www/shared/datepicker/scss/build3.scss")
|
||||
|
||||
bslib::bs_dependency(
|
||||
input = sass::sass_file(scss_file),
|
||||
theme = theme,
|
||||
name = "bootstrap-datepicker",
|
||||
version = datePickerVersion,
|
||||
cache_key_extra = shinyPackageVersion()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
|
||||
attachDependencies(
|
||||
div(id = inputId,
|
||||
class = "shiny-date-range-input form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
|
||||
shinyInputLabel(inputId, label),
|
||||
# input-daterange class is needed for dropdown behavior
|
||||
@@ -137,6 +137,6 @@ dateRangeInput <- function(inputId, label, start = NULL, end = NULL,
|
||||
)
|
||||
)
|
||||
),
|
||||
datePickerDependency
|
||||
datePickerDependency()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ fileInput <- function(inputId, label, multiple = FALSE, accept = NULL,
|
||||
|
||||
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
|
||||
div(class = "input-group",
|
||||
|
||||
@@ -45,7 +45,7 @@ numericInput <- function(inputId, label, value, min = NA, max = NA, step = NA,
|
||||
inputTag$attribs$step = step
|
||||
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
inputTag
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
passwordInput <- function(inputId, label, value = "", width = NULL,
|
||||
placeholder = NULL) {
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$input(id = inputId, type="password", class="form-control", value=value,
|
||||
placeholder = placeholder)
|
||||
|
||||
@@ -11,22 +11,22 @@
|
||||
#' @inheritParams textInput
|
||||
#' @param choices List of values to select from (if elements of the list are
|
||||
#' named then that name rather than the value is displayed to the user). If
|
||||
#' this argument is provided, then `choiceNames` and `choiceValues`
|
||||
#' must not be provided, and vice-versa. The values should be strings; other
|
||||
#' types (such as logicals and numbers) will be coerced to strings.
|
||||
#' @param selected The initially selected value (if not specified then defaults
|
||||
#' to the first value)
|
||||
#' this argument is provided, then `choiceNames` and `choiceValues` must not
|
||||
#' be provided, and vice-versa. The values should be strings; other types
|
||||
#' (such as logicals and numbers) will be coerced to strings.
|
||||
#' @param selected The initially selected value. If not specified, then it
|
||||
#' defaults to the first item in `choices`. To start with no items selected,
|
||||
#' use `character(0)`.
|
||||
#' @param inline If `TRUE`, render the choices inline (i.e. horizontally)
|
||||
#' @return A set of radio buttons that can be added to a UI definition.
|
||||
#' @param choiceNames,choiceValues List of names and values, respectively, that
|
||||
#' are displayed to the user in the app and correspond to the each choice (for
|
||||
#' this reason, `choiceNames` and `choiceValues` must have the same
|
||||
#' length). If either of these arguments is provided, then the other
|
||||
#' *must* be provided and `choices` *must not* be provided. The
|
||||
#' advantage of using both of these over a named list for `choices` is
|
||||
#' that `choiceNames` allows any type of UI object to be passed through
|
||||
#' (tag objects, icons, HTML code, ...), instead of just simple text. See
|
||||
#' Examples.
|
||||
#' this reason, `choiceNames` and `choiceValues` must have the same length).
|
||||
#' If either of these arguments is provided, then the other *must* be provided
|
||||
#' and `choices` *must not* be provided. The advantage of using both of these
|
||||
#' over a named list for `choices` is that `choiceNames` allows any type of UI
|
||||
#' object to be passed through (tag objects, icons, HTML code, ...), instead
|
||||
#' of just simple text. See Examples.
|
||||
#'
|
||||
#' @family input elements
|
||||
#' @seealso [updateRadioButtons()]
|
||||
@@ -82,7 +82,8 @@
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A character string containing the value of the selected button.
|
||||
#'
|
||||
#' A character string containing the value of the selected button.
|
||||
#'
|
||||
#' @export
|
||||
radioButtons <- function(inputId, label, choices = NULL, selected = NULL,
|
||||
@@ -103,10 +104,14 @@ radioButtons <- function(inputId, label, choices = NULL, selected = NULL,
|
||||
divClass <- "form-group shiny-input-radiogroup shiny-input-container"
|
||||
if (inline) divClass <- paste(divClass, "shiny-input-container-inline")
|
||||
|
||||
inputLabel <- shinyInputLabel(inputId, label)
|
||||
tags$div(id = inputId,
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
class = divClass,
|
||||
shinyInputLabel(inputId, label),
|
||||
# https://www.w3.org/TR/2017/WD-wai-aria-practices-1.1-20170628/examples/radio/radio-1/radio-1.html
|
||||
role = "radiogroup",
|
||||
`aria-labelledby` = inputLabel$attribs$id,
|
||||
inputLabel,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
106
R/input-select.R
106
R/input-select.R
@@ -12,6 +12,14 @@
|
||||
#' name will be treated as a placeholder prompt. For example:
|
||||
#' `selectInput("letter", "Letter", c("Choose one" = "", LETTERS))`
|
||||
#'
|
||||
#' **Performance note:** `selectInput()` and `selectizeInput()` can slow down
|
||||
#' significantly when thousands of choices are used; with legacy browsers like
|
||||
#' Internet Explorer, the user interface may hang for many seconds. For large
|
||||
#' numbers of choices, Shiny offers a "server-side selectize" option that
|
||||
#' massively improves performance and efficiency; see
|
||||
#' [this selectize article](https://shiny.rstudio.com/articles/selectize.html)
|
||||
#' on the Shiny Dev Center for details.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param choices List of values to select from. If elements of the list are
|
||||
#' named, then that name --- rather than the value --- is displayed to the
|
||||
@@ -100,7 +108,7 @@ selectInput <- function(inputId, label, choices, selected = NULL,
|
||||
id = inputId,
|
||||
class = if (!selectize) "form-control",
|
||||
size = size,
|
||||
selectOptions(choices, selected)
|
||||
selectOptions(choices, selected, inputId, selectize)
|
||||
)
|
||||
if (multiple)
|
||||
selectTag$attribs$multiple <- "multiple"
|
||||
@@ -108,7 +116,7 @@ selectInput <- function(inputId, label, choices, selected = NULL,
|
||||
# return label and select tag
|
||||
res <- div(
|
||||
class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
div(selectTag)
|
||||
)
|
||||
@@ -125,16 +133,22 @@ firstChoice <- function(choices) {
|
||||
}
|
||||
|
||||
# Create tags for each of the options; use <optgroup> if necessary.
|
||||
# This returns a HTML string instead of tags, because of the 'selected'
|
||||
# attribute.
|
||||
selectOptions <- function(choices, selected = NULL) {
|
||||
# This returns a HTML string instead of tags for performance reasons.
|
||||
selectOptions <- function(choices, selected = NULL, inputId, perfWarning = FALSE) {
|
||||
if (length(choices) >= 1000) {
|
||||
warning("The select input \"", inputId, "\" contains a large number of ",
|
||||
"options; consider using server-side selectize for massively improved ",
|
||||
"performance. See the Details section of the ?selectizeInput help topic.",
|
||||
call. = FALSE)
|
||||
}
|
||||
|
||||
html <- mapply(choices, names(choices), FUN = function(choice, label) {
|
||||
if (is.list(choice)) {
|
||||
# If sub-list, create an optgroup and recurse into the sublist
|
||||
sprintf(
|
||||
'<optgroup label="%s">\n%s\n</optgroup>',
|
||||
htmlEscape(label, TRUE),
|
||||
selectOptions(choice, selected)
|
||||
selectOptions(choice, selected, inputId, perfWarning)
|
||||
)
|
||||
|
||||
} else {
|
||||
@@ -183,6 +197,12 @@ selectizeInput <- function(inputId, ..., options = NULL, width = NULL) {
|
||||
|
||||
# given a select input and its id, selectize it
|
||||
selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
|
||||
if (length(options) == 0) {
|
||||
# For NULL and empty unnamed list, replace with an empty named list, so that
|
||||
# it will get translated to {} in JSON later on.
|
||||
options <- empty_named_list()
|
||||
}
|
||||
|
||||
# Make sure accessibility plugin is included
|
||||
if (!('selectize-plugin-a11y' %in% options$plugins)) {
|
||||
options$plugins <- c(options$plugins, list('selectize-plugin-a11y'))
|
||||
@@ -190,21 +210,17 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
|
||||
|
||||
res <- checkAsIs(options)
|
||||
|
||||
selectizeDep <- htmlDependency(
|
||||
"selectize", "0.12.4", c(href = "shared/selectize"),
|
||||
stylesheet = "css/selectize.bootstrap3.css",
|
||||
head = format(tagList(
|
||||
tags$script(src = 'shared/selectize/js/selectize.min.js'),
|
||||
# Accessibility plugin for screen readers (https://github.com/SLMNBJ/selectize-plugin-a11y):
|
||||
tags$script(src = 'shared/selectize/accessibility/js/selectize-plugin-a11y.min.js')
|
||||
))
|
||||
)
|
||||
deps <- list(selectizeDependency())
|
||||
|
||||
if ('drag_drop' %in% options$plugins) {
|
||||
selectizeDep <- list(selectizeDep, htmlDependency(
|
||||
'jqueryui', '1.12.1', c(href = 'shared/jqueryui'),
|
||||
script = 'jquery-ui.min.js'
|
||||
))
|
||||
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
|
||||
@@ -214,18 +230,64 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
|
||||
type = 'application/json',
|
||||
`data-for` = inputId, `data-nonempty` = if (nonempty) '',
|
||||
`data-eval` = if (length(res$eval)) HTML(toJSON(res$eval)),
|
||||
if (length(res$options)) HTML(toJSON(res$options)) else '{}'
|
||||
HTML(toJSON(res$options))
|
||||
)
|
||||
)
|
||||
|
||||
attachDependencies(select, selectizeDep)
|
||||
attachDependencies(select, deps)
|
||||
}
|
||||
|
||||
|
||||
selectizeDependency <- function() {
|
||||
bslib::bs_dependency_defer(selectizeDependencyFunc)
|
||||
}
|
||||
|
||||
selectizeDependencyFunc <- function(theme) {
|
||||
selectizeVersion <- "0.12.4"
|
||||
if (!is_bs_theme(theme)) {
|
||||
return(selectizeStaticDependency(selectizeVersion))
|
||||
}
|
||||
|
||||
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,
|
||||
# HTML dependency (which we currently do for other themable widgets),
|
||||
# but DT, crosstalk, and maybe other pkgs include selectize JS/CSS
|
||||
# in HTML dependency named selectize, so if we were to change that
|
||||
# name, the JS/CSS would be loaded/included twice, which leads to
|
||||
# strange issues, especially since we now include a 3rd party
|
||||
# accessibility plugin https://github.com/rstudio/shiny/pull/3153
|
||||
script <- file.path(
|
||||
selectizeDir, c("js/selectize.min.js", "accessibility/js/selectize-plugin-a11y.min.js")
|
||||
)
|
||||
bslib::bs_dependency(
|
||||
input = sass::sass_file(stylesheet),
|
||||
theme = theme,
|
||||
name = "selectize",
|
||||
version = selectizeVersion,
|
||||
cache_key_extra = shinyPackageVersion(),
|
||||
.dep_args = list(script = script)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
selectizeStaticDependency <- function(version) {
|
||||
htmlDependency(
|
||||
"selectize", version,
|
||||
src = c(href = "shared/selectize"),
|
||||
stylesheet = "css/selectize.bootstrap3.css",
|
||||
script = c(
|
||||
"js/selectize.min.js",
|
||||
"accessibility/js/selectize-plugin-a11y.min.js"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#' Select variables from a data frame
|
||||
|
||||
140
R/input-slider.R
140
R/input-slider.R
@@ -1,25 +1,24 @@
|
||||
#' Slider Input Widget
|
||||
#'
|
||||
#' Constructs a slider widget to select a numeric value from a range.
|
||||
#' Constructs a slider widget to select a number, date, or date-time from a
|
||||
#' range.
|
||||
#'
|
||||
#' @inheritParams textInput
|
||||
#' @param min The minimum value (inclusive) that can be selected.
|
||||
#' @param max The maximum value (inclusive) that can be selected.
|
||||
#' @param value The initial value of the slider. A numeric vector of length one
|
||||
#' will create a regular slider; a numeric vector of length two will create a
|
||||
#' double-ended range slider. A warning will be issued if the value doesn't
|
||||
#' fit between `min` and `max`.
|
||||
#' @param min,max The minimum and maximum values (inclusive) that can be
|
||||
#' selected.
|
||||
#' @param value The initial value of the slider, either a number, a date
|
||||
#' (class Date), or a date-time (class POSIXt). A length one vector will
|
||||
#' create a regular slider; a length two vector will create a double-ended
|
||||
#' range slider. Must lie between `min` and `max`.
|
||||
#' @param step Specifies the interval between each selectable value on the
|
||||
#' slider (if `NULL`, a heuristic is used to determine the step size). If
|
||||
#' the values are dates, `step` is in days; if the values are times
|
||||
#' (POSIXt), `step` is in seconds.
|
||||
#' slider. Either `NULL`, the default, which uses a heuristic to determine the
|
||||
#' step size or a single number. If the values are dates, `step` is in days;
|
||||
#' if the values are date-times, `step` is in seconds.
|
||||
#' @param round `TRUE` to round all values to the nearest integer;
|
||||
#' `FALSE` if no rounding is desired; or an integer to round to that
|
||||
#' number of digits (for example, 1 will round to the nearest 10, and -2 will
|
||||
#' round to the nearest .01). Any rounding will be applied after snapping to
|
||||
#' the nearest step.
|
||||
#' @param format Deprecated.
|
||||
#' @param locale Deprecated.
|
||||
#' @param ticks `FALSE` to hide tick marks, `TRUE` to show them
|
||||
#' according to some simple heuristics.
|
||||
#' @param animate `TRUE` to show simple animation controls with default
|
||||
@@ -72,23 +71,15 @@
|
||||
#' }
|
||||
#'
|
||||
#' @section Server value:
|
||||
#' A number, or in the case of slider range, a vector of two numbers.
|
||||
#' A number, date, or date-time (depending on the class of `value`), or
|
||||
#' in the case of slider range, a vector of two numbers/dates/date-times.
|
||||
#'
|
||||
#' @export
|
||||
sliderInput <- function(inputId, label, min, max, value, step = NULL,
|
||||
round = FALSE, format = NULL, locale = NULL,
|
||||
ticks = TRUE, animate = FALSE, width = NULL, sep = ",",
|
||||
pre = NULL, post = NULL, timeFormat = NULL,
|
||||
timezone = NULL, dragRange = TRUE)
|
||||
{
|
||||
if (!missing(format)) {
|
||||
shinyDeprecated(msg = "The `format` argument to sliderInput is deprecated. Use `sep`, `pre`, and `post` instead.",
|
||||
version = "0.10.2.2")
|
||||
}
|
||||
if (!missing(locale)) {
|
||||
shinyDeprecated(msg = "The `locale` argument to sliderInput is deprecated. Use `sep`, `pre`, and `post` instead.",
|
||||
version = "0.10.2.2")
|
||||
}
|
||||
round = FALSE, ticks = TRUE, animate = FALSE,
|
||||
width = NULL, sep = ",", pre = NULL, post = NULL,
|
||||
timeFormat = NULL, timezone = NULL, dragRange = TRUE) {
|
||||
validate_slider_value(min, max, value, "sliderInput")
|
||||
|
||||
dataType <- getSliderType(min, max, value)
|
||||
|
||||
@@ -144,6 +135,7 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
|
||||
sliderProps <- dropNulls(list(
|
||||
class = "js-range-slider",
|
||||
id = inputId,
|
||||
`data-skin` = "shiny",
|
||||
`data-type` = if (length(value) > 1) "double",
|
||||
`data-min` = formatNoSci(min),
|
||||
`data-max` = formatNoSci(max),
|
||||
@@ -175,7 +167,7 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
|
||||
})
|
||||
|
||||
sliderTag <- div(class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
do.call(tags$input, sliderProps)
|
||||
)
|
||||
@@ -205,20 +197,64 @@ sliderInput <- function(inputId, label, min, max, value, step = NULL,
|
||||
)
|
||||
}
|
||||
|
||||
dep <- list(
|
||||
htmlDependency("ionrangeslider", "2.1.6", c(href="shared/ionrangeslider"),
|
||||
script = "js/ion.rangeSlider.min.js",
|
||||
# ion.rangeSlider also needs normalize.css, which is already included in
|
||||
# Bootstrap.
|
||||
stylesheet = c("css/ion.rangeSlider.css",
|
||||
"css/ion.rangeSlider.skinShiny.css")
|
||||
attachDependencies(sliderTag, ionRangeSliderDependency())
|
||||
}
|
||||
|
||||
|
||||
ionRangeSliderVersion <- "2.3.1"
|
||||
|
||||
ionRangeSliderDependency <- function() {
|
||||
list(
|
||||
# ion.rangeSlider also needs normalize.css, which is already included in Bootstrap.
|
||||
htmlDependency(
|
||||
"ionrangeslider-javascript", ionRangeSliderVersion,
|
||||
src = c(href = "shared/ionrangeslider"),
|
||||
script = "js/ion.rangeSlider.min.js"
|
||||
),
|
||||
htmlDependency("strftime", "0.9.2", c(href="shared/strftime"),
|
||||
htmlDependency(
|
||||
"strftime", "0.9.2",
|
||||
src = c(href = "shared/strftime"),
|
||||
script = "strftime-min.js"
|
||||
),
|
||||
bslib::bs_dependency_defer(ionRangeSliderDependencyCSS)
|
||||
)
|
||||
}
|
||||
|
||||
ionRangeSliderDependencyCSS <- function(theme) {
|
||||
if (!is_bs_theme(theme)) {
|
||||
return(htmlDependency(
|
||||
"ionrangeslider-css",
|
||||
ionRangeSliderVersion,
|
||||
src = c(href = "shared/ionrangeslider"),
|
||||
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")
|
||||
)
|
||||
)
|
||||
|
||||
attachDependencies(sliderTag, dep)
|
||||
bslib::bs_dependency(
|
||||
input = sass_input,
|
||||
theme = theme,
|
||||
name = "ionRangeSlider",
|
||||
version = ionRangeSliderVersion,
|
||||
cache_key_extra = shinyPackageVersion()
|
||||
)
|
||||
}
|
||||
|
||||
hasDecimals <- function(value) {
|
||||
@@ -226,7 +262,6 @@ hasDecimals <- function(value) {
|
||||
return (!identical(value, truncatedValue))
|
||||
}
|
||||
|
||||
|
||||
# If step is NULL, use heuristic to set the step size.
|
||||
findStepSize <- function(min, max, step) {
|
||||
if (!is.null(step)) return(step)
|
||||
@@ -253,6 +288,37 @@ findStepSize <- function(min, max, step) {
|
||||
}
|
||||
}
|
||||
|
||||
# Throw a warning if ever `value` is not in the [`min`, `max`] range
|
||||
validate_slider_value <- function(min, max, value, fun) {
|
||||
if (length(min) != 1 || is_na(min) ||
|
||||
length(max) != 1 || is_na(max) ||
|
||||
length(value) < 1 || length(value) > 2 || any(is.na(value)))
|
||||
{
|
||||
stop(call. = FALSE,
|
||||
sprintf("In %s(): `min`, `max`, and `value` cannot be NULL, NA, or empty.", fun)
|
||||
)
|
||||
}
|
||||
|
||||
if (min(value) < min) {
|
||||
warning(call. = FALSE,
|
||||
sprintf(
|
||||
"In %s(): `value` should be greater than or equal to `min` (value = %s, min = %s).",
|
||||
fun, paste(value, collapse = ", "), min
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (max(value) > max) {
|
||||
warning(
|
||||
noBreaks. = TRUE, call. = FALSE,
|
||||
sprintf(
|
||||
"In %s(): `value` should be less than or equal to `max` (value = %s, max = %s).",
|
||||
fun, paste(value, collapse = ", "), max
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#' @rdname sliderInput
|
||||
#'
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#' [actionButton()] instead of `submitButton` when you
|
||||
#' want to delay a reaction.
|
||||
#' See [this
|
||||
#' article](http://shiny.rstudio.com/articles/action-buttons.html) for more information (including a demo of how to "translate"
|
||||
#' article](https://shiny.rstudio.com/articles/action-buttons.html) for more information (including a demo of how to "translate"
|
||||
#' code using a `submitButton` to code using an `actionButton`).
|
||||
#'
|
||||
#' In essence, the presence of a submit button stops all inputs from
|
||||
@@ -58,7 +58,7 @@ submitButton <- function(text = "Apply Changes", icon = NULL, width = NULL) {
|
||||
tags$button(
|
||||
type="submit",
|
||||
class="btn btn-primary",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
list(icon, text)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ textInput <- function(inputId, label, value = "", width = NULL,
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$input(id = inputId, type="text", class="form-control", value=value,
|
||||
placeholder = placeholder)
|
||||
|
||||
@@ -50,18 +50,16 @@ textAreaInput <- function(inputId, label, value = "", width = NULL, height = NUL
|
||||
resize <- match.arg(resize, c("both", "none", "vertical", "horizontal"))
|
||||
}
|
||||
|
||||
style <- paste(
|
||||
if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
if (!is.null(height)) paste0("height: ", validateCssUnit(height), ";"),
|
||||
if (!is.null(resize)) paste0("resize: ", resize, ";")
|
||||
style <- css(
|
||||
# The width is specified on the parent div.
|
||||
width = if (!is.null(width)) "width: 100%;",
|
||||
height = validateCssUnit(height),
|
||||
resize = resize
|
||||
)
|
||||
|
||||
# Workaround for tag attribute=character(0) bug:
|
||||
# https://github.com/rstudio/htmltools/issues/65
|
||||
if (length(style) == 0) style <- NULL
|
||||
|
||||
div(class = "form-group shiny-input-container",
|
||||
shinyInputLabel(inputId, label),
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
tags$textarea(
|
||||
id = inputId,
|
||||
class = "form-control",
|
||||
|
||||
80
R/knitr.R
Normal file
80
R/knitr.R
Normal file
@@ -0,0 +1,80 @@
|
||||
#' Knitr S3 methods
|
||||
#'
|
||||
#' These S3 methods are necessary to help Shiny applications and UI chunks embed
|
||||
#' themselves in knitr/rmarkdown documents.
|
||||
#'
|
||||
#' @name knitr_methods
|
||||
#' @param x Object to knit_print
|
||||
#' @param ... Additional knit_print arguments
|
||||
NULL
|
||||
|
||||
# If there's an R Markdown runtime option set but it isn't set to Shiny, then
|
||||
# return a warning indicating the runtime is inappropriate for this object.
|
||||
# Returns NULL in all other cases.
|
||||
shiny_rmd_warning <- function() {
|
||||
runtime <- knitr::opts_knit$get("rmarkdown.runtime")
|
||||
if (!is.null(runtime) && runtime != "shiny")
|
||||
# note that the RStudio IDE checks for this specific string to detect Shiny
|
||||
# applications in static document
|
||||
list(structure(
|
||||
"Shiny application in a static R Markdown document",
|
||||
class = "rmd_warning"))
|
||||
else
|
||||
NULL
|
||||
}
|
||||
|
||||
#' @rdname knitr_methods
|
||||
knit_print.shiny.appobj <- function(x, ...) {
|
||||
opts <- x$options %||% list()
|
||||
width <- if (is.null(opts$width)) "100%" else opts$width
|
||||
height <- if (is.null(opts$height)) "400" else opts$height
|
||||
|
||||
runtime <- knitr::opts_knit$get("rmarkdown.runtime")
|
||||
if (!is.null(runtime) && runtime != "shiny") {
|
||||
# If not rendering to a Shiny document, create a box exactly the same
|
||||
# dimensions as the Shiny app would have had (so the document continues to
|
||||
# flow as it would have with the app), and display a diagnostic message
|
||||
width <- validateCssUnit(width)
|
||||
height <- validateCssUnit(height)
|
||||
output <- tags$div(
|
||||
style=paste("width:", width, "; height:", height, "; text-align: center;",
|
||||
"box-sizing: border-box;", "-moz-box-sizing: border-box;",
|
||||
"-webkit-box-sizing: border-box;"),
|
||||
class="muted well",
|
||||
"Shiny applications not supported in static R Markdown documents")
|
||||
}
|
||||
else {
|
||||
path <- addSubApp(x)
|
||||
output <- deferredIFrame(path, width, height)
|
||||
}
|
||||
|
||||
# If embedded Shiny apps ever have JS/CSS dependencies (like pym.js) we'll
|
||||
# need to grab those and put them in meta, like in knit_print.shiny.tag. But
|
||||
# for now it's not an issue, so just return the HTML and warning.
|
||||
|
||||
knitr::asis_output(htmlPreserve(format(output, indent=FALSE)),
|
||||
meta = shiny_rmd_warning(), cacheable = FALSE)
|
||||
}
|
||||
|
||||
# Let us use a nicer syntax in knitr chunks than literally
|
||||
# calling output$value <- renderFoo(...) and fooOutput().
|
||||
#' @rdname knitr_methods
|
||||
#' @param inline Whether the object is printed inline.
|
||||
knit_print.shiny.render.function <- function(x, ..., inline = FALSE) {
|
||||
x <- htmltools::as.tags(x, inline = inline)
|
||||
output <- knitr::knit_print(tagList(x))
|
||||
attr(output, "knit_cacheable") <- FALSE
|
||||
attr(output, "knit_meta") <- append(attr(output, "knit_meta"),
|
||||
shiny_rmd_warning())
|
||||
output
|
||||
}
|
||||
|
||||
# Lets us drop reactive expressions directly into a knitr chunk and have the
|
||||
# value printed out! Nice for teaching if nothing else.
|
||||
#' @rdname knitr_methods
|
||||
knit_print.reactive <- function(x, ..., inline = FALSE) {
|
||||
renderFunc <- if (inline) renderText else renderPrint
|
||||
knitr::knit_print(renderFunc({
|
||||
x()
|
||||
}), inline = inline)
|
||||
}
|
||||
15
R/map.R
15
R/map.R
@@ -1,18 +1,3 @@
|
||||
# TESTS
|
||||
# Simple set/get
|
||||
# Simple remove
|
||||
# Simple containsKey
|
||||
# Simple keys
|
||||
# Simple values
|
||||
# Simple clear
|
||||
# Get of unknown key returns NULL
|
||||
# Remove of unknown key does nothing
|
||||
# Setting a key twice always results in last-one-wins
|
||||
# /TESTS
|
||||
|
||||
# Note that Map objects can't be saved in one R session and restored in
|
||||
# another, because they are based on fastmap, which uses an external pointer,
|
||||
# and external pointers can't be saved and restored in another session.
|
||||
#' @importFrom fastmap fastmap
|
||||
Map <- R6Class(
|
||||
'Map',
|
||||
|
||||
@@ -309,7 +309,7 @@ HandlerManager <- R6Class("HandlerManager",
|
||||
createHttpuvApp = function() {
|
||||
list(
|
||||
onHeaders = function(req) {
|
||||
maxSize <- getOption('shiny.maxRequestSize') %OR% (5 * 1024 * 1024)
|
||||
maxSize <- getOption('shiny.maxRequestSize') %||% (5 * 1024 * 1024)
|
||||
if (maxSize <= 0)
|
||||
return(NULL)
|
||||
|
||||
@@ -346,7 +346,7 @@ HandlerManager <- R6Class("HandlerManager",
|
||||
),
|
||||
catch = function(err) {
|
||||
httpResponse(status = 500L,
|
||||
content_type = "text/html",
|
||||
content_type = "text/html; charset=UTF-8",
|
||||
content = as.character(htmltools::htmlTemplate(
|
||||
system.file("template", "error.html", package = "shiny"),
|
||||
message = conditionMessage(err)
|
||||
@@ -426,7 +426,7 @@ HandlerManager <- R6Class("HandlerManager",
|
||||
)
|
||||
|
||||
maybeInjectAutoreload <- function(resp) {
|
||||
if (getOption("shiny.autoreload", FALSE) &&
|
||||
if (get_devmode_option("shiny.autoreload", FALSE) &&
|
||||
isTRUE(grepl("^text/html($|;)", resp$content_type)) &&
|
||||
is.character(resp$content)) {
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ wait_for_it <- function() {
|
||||
|
||||
# Block until the promise is resolved/rejected. If resolved, return the value.
|
||||
# If rejected, throw (yes throw, not return) the error.
|
||||
#' @importFrom promises %...!%
|
||||
#' @importFrom promises %...>%
|
||||
extract <- function(promise) {
|
||||
promise_value <- NULL
|
||||
error <- NULL
|
||||
@@ -31,7 +29,6 @@ extract <- function(promise) {
|
||||
#' @noRd
|
||||
#' @export
|
||||
`$.mockclientdata` <- function(x, name) {
|
||||
if (name == "allowDataUriScheme") { return(TRUE) }
|
||||
if (name == "pixelratio") { return(1) }
|
||||
if (name == "url_protocol") { return("http:") }
|
||||
if (name == "url_hostname") { return("mocksession") }
|
||||
@@ -156,6 +153,8 @@ makeExtraMethods <- function() {
|
||||
"sendInsertTab",
|
||||
"sendInsertUI",
|
||||
"sendModal",
|
||||
"setCurrentTheme",
|
||||
"getCurrentTheme",
|
||||
"sendNotification",
|
||||
"sendProgress",
|
||||
"sendRemoveTab",
|
||||
@@ -167,12 +166,10 @@ makeExtraMethods <- function() {
|
||||
), makeErrors(
|
||||
`@uploadEnd` = "for internal use only",
|
||||
`@uploadInit` = "for internal use only",
|
||||
`@uploadieFinish` = "for internal use only",
|
||||
createBookmarkObservers = "for internal use only",
|
||||
dispatch = "for internal use only",
|
||||
handleRequest = "for internal use only",
|
||||
requestFlush = "for internal use only",
|
||||
saveFileUrl = "for internal use only",
|
||||
startTiming = "for internal use only",
|
||||
wsClosed = "for internal use only"
|
||||
))
|
||||
@@ -236,9 +233,9 @@ MockShinySession <- R6Class(
|
||||
progressStack = 'Stack',
|
||||
#' @field token On a real `ShinySession`, used to identify this instance in URLs.
|
||||
token = 'character',
|
||||
#' @field cache The session cache MemoryCache.
|
||||
#' @field cache The session cache object.
|
||||
cache = NULL,
|
||||
#' @field appcache The app cache MemoryCache.
|
||||
#' @field appcache The app cache object.
|
||||
appcache = NULL,
|
||||
#' @field restoreContext Part of bookmarking support in a real
|
||||
#' `ShinySession` but always `NULL` for a `MockShinySession`.
|
||||
@@ -249,6 +246,8 @@ MockShinySession <- R6Class(
|
||||
#' @field user The username of an authenticated user. Always `NULL` for a
|
||||
#' `MockShinySession`.
|
||||
user = NULL,
|
||||
#' @field options A list containing session-level shinyOptions.
|
||||
options = NULL,
|
||||
|
||||
#' @description Create a new MockShinySession.
|
||||
initialize = function() {
|
||||
@@ -260,7 +259,7 @@ MockShinySession <- R6Class(
|
||||
private$file_generators <- fastmap()
|
||||
|
||||
private$timer <- MockableTimerCallbacks$new()
|
||||
self$progressStack <- Stack$new()
|
||||
self$progressStack <- fastmap::faststack()
|
||||
|
||||
self$userData <- new.env(parent=emptyenv())
|
||||
|
||||
@@ -273,8 +272,12 @@ MockShinySession <- R6Class(
|
||||
self$input <- .createReactiveValues(private$.input, readonly = TRUE)
|
||||
|
||||
self$token <- createUniqueId(16)
|
||||
self$cache <- MemoryCache$new()
|
||||
self$appcache <- MemoryCache$new()
|
||||
|
||||
# Copy app-level options
|
||||
self$options <- getCurrentAppState()$options
|
||||
|
||||
self$cache <- cachem::cache_mem()
|
||||
self$appcache <- cachem::cache_mem()
|
||||
|
||||
# Adds various generated noop and error-producing method implementations.
|
||||
# Note that noop methods can be configured to produce warnings by setting
|
||||
|
||||
21
R/modal.R
21
R/modal.R
@@ -29,10 +29,16 @@ removeModal <- function(session = getDefaultReactiveDomain()) {
|
||||
|
||||
#' Create a modal dialog UI
|
||||
#'
|
||||
#' This creates the UI for a modal dialog, using Bootstrap's modal class. Modals
|
||||
#' are typically used for showing important messages, or for presenting UI that
|
||||
#' requires input from the user, such as a username and password input.
|
||||
#' @description
|
||||
#' `modalDialog()` creates the UI for a modal dialog, using Bootstrap's modal
|
||||
#' class. Modals are typically used for showing important messages, or for
|
||||
#' presenting UI that requires input from the user, such as a user name and
|
||||
#' password input.
|
||||
#'
|
||||
#' `modalButton()` creates a button that will dismiss the dialog when clicked,
|
||||
#' typically used when customising the `footer`.
|
||||
#'
|
||||
#' @inheritParams actionButton
|
||||
#' @param ... UI elements for the body of the modal dialog box.
|
||||
#' @param title An optional title for the dialog.
|
||||
#' @param footer UI for footer. Use `NULL` for no footer.
|
||||
@@ -41,7 +47,7 @@ removeModal <- function(session = getDefaultReactiveDomain()) {
|
||||
#' @param easyClose If `TRUE`, the modal dialog can be dismissed by
|
||||
#' clicking outside the dialog box, or be pressing the Escape key. If
|
||||
#' `FALSE` (the default), the modal dialog can't be dismissed in those
|
||||
#' ways; instead it must be dismissed by clicking on the dismiss button, or
|
||||
#' ways; instead it must be dismissed by clicking on a `modalButton()`, or
|
||||
#' from a call to [removeModal()] on the server.
|
||||
#' @param fade If `FALSE`, the modal dialog will have no fade-in animation
|
||||
#' (it will simply appear rather than fade in to view).
|
||||
@@ -169,13 +175,8 @@ modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"),
|
||||
)
|
||||
}
|
||||
|
||||
#' Create a button for a modal dialog
|
||||
#'
|
||||
#' When clicked, a `modalButton` will dismiss the modal dialog.
|
||||
#'
|
||||
#' @inheritParams actionButton
|
||||
#' @seealso [modalDialog()] for examples.
|
||||
#' @export
|
||||
#' @rdname modalDialog
|
||||
modalButton <- function(label, icon = NULL) {
|
||||
tags$button(type = "button", class = "btn btn-default",
|
||||
`data-dismiss` = "modal", validateIcon(icon), label
|
||||
|
||||
29
R/modules.R
29
R/modules.R
@@ -31,17 +31,42 @@ createSessionProxy <- function(parentSession, ...) {
|
||||
# but not `session$userData <- TRUE`) from within a module
|
||||
# without any hacks (see PR #1732)
|
||||
if (identical(x[[name]], value)) return(x)
|
||||
|
||||
# Special case for $options (issue #3112)
|
||||
if (name == "options") {
|
||||
session <- find_ancestor_session(x)
|
||||
session[[name]] <- value
|
||||
return(x)
|
||||
}
|
||||
|
||||
stop("Attempted to assign value on session proxy.")
|
||||
}
|
||||
|
||||
`[[<-.session_proxy` <- `$<-.session_proxy`
|
||||
|
||||
# Given a session_proxy, search `parent` recursively to find the real
|
||||
# ShinySession object. If given a ShinySession, simply return it.
|
||||
find_ancestor_session <- function(x, depth = 20) {
|
||||
if (depth < 0) {
|
||||
stop("ShinySession not found")
|
||||
}
|
||||
if (inherits(x, "ShinySession")) {
|
||||
return(x)
|
||||
}
|
||||
if (inherits(x, "session_proxy")) {
|
||||
return(find_ancestor_session(.subset2(x, "parent"), depth-1))
|
||||
}
|
||||
|
||||
stop("ShinySession not found")
|
||||
}
|
||||
|
||||
|
||||
#' Shiny modules
|
||||
#'
|
||||
#' Shiny's module feature lets you break complicated UI and server logic into
|
||||
#' smaller, self-contained pieces. Compared to large monolithic Shiny apps,
|
||||
#' modules are easier to reuse and easier to reason about. See the article at
|
||||
#' <http://shiny.rstudio.com/articles/modules.html> to learn more.
|
||||
#' <https://shiny.rstudio.com/articles/modules.html> to learn more.
|
||||
#'
|
||||
#' Starting in Shiny 1.5.0, we recommend using `moduleServer` instead of
|
||||
#' [`callModule()`], because the syntax is a little easier
|
||||
@@ -55,7 +80,7 @@ createSessionProxy <- function(parentSession, ...) {
|
||||
#' almost always be used).
|
||||
#'
|
||||
#' @return The return value, if any, from executing the module server function
|
||||
#' @seealso <http://shiny.rstudio.com/articles/modules.html>
|
||||
#' @seealso <https://shiny.rstudio.com/articles/modules.html>
|
||||
#'
|
||||
#' @examples
|
||||
#' # Define the UI for a module
|
||||
|
||||
@@ -76,8 +76,10 @@ Progress <- R6Class(
|
||||
min = 0, max = 1,
|
||||
style = getShinyOption("progress.style", default = "notification"))
|
||||
{
|
||||
if (is.null(session))
|
||||
rlang::abort("Can only use Progress$new() inside a Shiny app")
|
||||
if (is.null(session$progressStack))
|
||||
stop("'session' is not a ShinySession object.")
|
||||
rlang::abort("`session` is not a ShinySession object.")
|
||||
|
||||
private$session <- session
|
||||
private$id <- createUniqueId(8)
|
||||
@@ -204,7 +206,7 @@ Progress <- R6Class(
|
||||
#' the server function. The default is to automatically find the session by
|
||||
#' using the current reactive domain.
|
||||
#' @param expr The work to be done. This expression should contain calls to
|
||||
#' `setProgress`.
|
||||
#' [setProgress()] or [incProgress()].
|
||||
#' @param min The value that represents the starting point of the progress bar.
|
||||
#' Must be less tham `max`. Default is 0.
|
||||
#' @param max The value that represents the end of the progress bar. Must be
|
||||
@@ -227,6 +229,7 @@ Progress <- R6Class(
|
||||
#' @param value Single-element numeric vector; the value at which to set the
|
||||
#' progress bar, relative to `min` and `max`.
|
||||
#'
|
||||
#' @return The result of `expr`.
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
|
||||
19
R/react.R
19
R/react.R
@@ -5,7 +5,7 @@ processId <- local({
|
||||
cached <- NULL
|
||||
function() {
|
||||
if (is.null(cached)) {
|
||||
cached <<- digest::digest(list(
|
||||
cached <<- rlang::hash(list(
|
||||
Sys.info(),
|
||||
Sys.time()
|
||||
))
|
||||
@@ -65,7 +65,7 @@ Context <- R6Class(
|
||||
that have been registered with onInvalidate()."
|
||||
|
||||
if (!identical(.pid, processId())) {
|
||||
stop("Reactive context was created in one process and invalidated from another")
|
||||
rlang::abort("Reactive context was created in one process and invalidated from another.")
|
||||
}
|
||||
|
||||
if (.invalidated)
|
||||
@@ -87,7 +87,7 @@ Context <- R6Class(
|
||||
immediately."
|
||||
|
||||
if (!identical(.pid, processId())) {
|
||||
stop("Reactive context was created in one process and accessed from another")
|
||||
rlang::abort("Reactive context was created in one process and accessed from another.")
|
||||
}
|
||||
|
||||
if (.invalidated)
|
||||
@@ -140,9 +140,13 @@ ReactiveEnvironment <- R6Class(
|
||||
if (isTRUE(getOption('shiny.suppressMissingContextError'))) {
|
||||
return(getDummyContext())
|
||||
} else {
|
||||
stop('Operation not allowed without an active reactive context. ',
|
||||
'(You tried to do something that can only be done from inside a ',
|
||||
'reactive expression or observer.)')
|
||||
rlang::abort(c(
|
||||
'Operation not allowed without an active reactive context.',
|
||||
paste0(
|
||||
'You tried to do something that can only be done from inside a ',
|
||||
'reactive consumer.'
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
return(.currentContext)
|
||||
@@ -202,7 +206,8 @@ getCurrentContext <- function() {
|
||||
.getReactiveEnvironment()$currentContext()
|
||||
}
|
||||
hasCurrentContext <- function() {
|
||||
!is.null(.getReactiveEnvironment()$.currentContext)
|
||||
!is.null(.getReactiveEnvironment()$.currentContext) ||
|
||||
isTRUE(getOption("shiny.suppressMissingContextError"))
|
||||
}
|
||||
|
||||
getDummyContext <- function() {
|
||||
|
||||
360
R/reactives.R
360
R/reactives.R
@@ -105,9 +105,7 @@ ReactiveVal <- R6Class(
|
||||
invisible(TRUE)
|
||||
},
|
||||
freeze = function(session = getDefaultReactiveDomain()) {
|
||||
if (is.null(session)) {
|
||||
stop("Can't freeze a reactiveVal without a reactive domain")
|
||||
}
|
||||
checkReactiveDomain(session)
|
||||
rLog$freezeReactiveVal(private$reactId, session)
|
||||
session$onFlushed(function() {
|
||||
self$thaw(session)
|
||||
@@ -231,18 +229,30 @@ reactiveVal <- function(value = NULL, label = NULL) {
|
||||
#' @rdname freezeReactiveValue
|
||||
#' @export
|
||||
freezeReactiveVal <- function(x) {
|
||||
domain <- getDefaultReactiveDomain()
|
||||
if (is.null(domain)) {
|
||||
stop("freezeReactiveVal() must be called when a default reactive domain is active.")
|
||||
if (getOption("shiny.deprecation.messages", TRUE) && getOption("shiny.deprecation.messages.freeze", TRUE)) {
|
||||
rlang::warn(
|
||||
"freezeReactiveVal() is soft-deprecated, and may be removed in a future version of Shiny. (See https://github.com/rstudio/shiny/issues/3063)",
|
||||
.frequency = "once", .frequency_id = "freezeReactiveVal")
|
||||
}
|
||||
|
||||
domain <- getDefaultReactiveDomain()
|
||||
checkReactiveDomain(domain)
|
||||
|
||||
if (!inherits(x, "reactiveVal")) {
|
||||
stop("x must be a reactiveVal object")
|
||||
rlang::abort("`x` must be a reactiveVal.")
|
||||
}
|
||||
|
||||
attr(x, ".impl", exact = TRUE)$freeze(domain)
|
||||
invisible()
|
||||
}
|
||||
|
||||
checkReactiveDomain <- function(x) {
|
||||
if (is.null(x)) {
|
||||
rlang::abort("Can't freeze reactive values without a reactive domain.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
format.reactiveVal <- function(x, ...) {
|
||||
attr(x, ".impl", exact = TRUE)$format(...)
|
||||
@@ -357,7 +367,7 @@ ReactiveValues <- R6Class(
|
||||
keyValue
|
||||
},
|
||||
|
||||
set = function(key, value) {
|
||||
set = function(key, value, force = FALSE) {
|
||||
# if key exists
|
||||
# if it is the same value, return
|
||||
#
|
||||
@@ -389,10 +399,8 @@ ReactiveValues <- R6Class(
|
||||
|
||||
key_exists <- .values$containsKey(key)
|
||||
|
||||
if (key_exists) {
|
||||
if (.dedupe && identical(.values$get(key), value)) {
|
||||
return(invisible())
|
||||
}
|
||||
if (key_exists && !isTRUE(force) && .dedupe && identical(.values$get(key), value)) {
|
||||
return(invisible())
|
||||
}
|
||||
|
||||
# set the value for better logging
|
||||
@@ -469,10 +477,15 @@ ReactiveValues <- R6Class(
|
||||
|
||||
# Mark a value as frozen If accessed while frozen, a shiny.silent.error will
|
||||
# be thrown.
|
||||
freeze = function(key) {
|
||||
freeze = function(key, invalidate = FALSE) {
|
||||
domain <- getDefaultReactiveDomain()
|
||||
rLog$freezeReactiveKey(.reactId, key, domain)
|
||||
setMeta(key, "frozen", TRUE)
|
||||
|
||||
if (invalidate) {
|
||||
# Force an invalidation
|
||||
self$set(key, NULL, force = TRUE)
|
||||
}
|
||||
},
|
||||
|
||||
thaw = function(key) {
|
||||
@@ -557,7 +570,7 @@ ReactiveValues <- R6Class(
|
||||
reactiveValues <- function(...) {
|
||||
args <- list(...)
|
||||
if ((length(args) > 0) && (is.null(names(args)) || any(names(args) == "")))
|
||||
stop("All arguments passed to reactiveValues() must be named.")
|
||||
rlang::abort("All arguments passed to reactiveValues() must be named.")
|
||||
|
||||
values <- .createReactiveValues(ReactiveValues$new())
|
||||
|
||||
@@ -568,7 +581,7 @@ reactiveValues <- function(...) {
|
||||
|
||||
checkName <- function(x) {
|
||||
if (!is.character(x) || length(x) != 1) {
|
||||
stop("Must use single string to index into reactivevalues")
|
||||
rlang::abort("Must use single string to index into reactivevalues.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,6 +623,14 @@ is.reactivevalues <- function(x) inherits(x, 'reactivevalues')
|
||||
#' @export
|
||||
`$.reactivevalues` <- function(x, name) {
|
||||
checkName(name)
|
||||
|
||||
if (!hasCurrentContext()) {
|
||||
rlang::abort(c(
|
||||
paste0("Can't access reactive value '", name, "' outside of reactive consumer."),
|
||||
i = "Do you need to wrap inside reactive() or observe()?"
|
||||
))
|
||||
}
|
||||
|
||||
.subset2(x, 'impl')$get(.subset2(x, 'ns')(name))
|
||||
}
|
||||
|
||||
@@ -619,7 +640,7 @@ is.reactivevalues <- function(x) inherits(x, 'reactivevalues')
|
||||
#' @export
|
||||
`$<-.reactivevalues` <- function(x, name, value) {
|
||||
if (.subset2(x, 'readonly')) {
|
||||
stop("Attempted to assign value to a read-only reactivevalues object")
|
||||
rlang::abort(paste0("Can't modify read-only reactive value '", name, "'"))
|
||||
}
|
||||
checkName(name)
|
||||
.subset2(x, 'impl')$set(.subset2(x, 'ns')(name), value)
|
||||
@@ -631,12 +652,12 @@ is.reactivevalues <- function(x) inherits(x, 'reactivevalues')
|
||||
|
||||
#' @export
|
||||
`[.reactivevalues` <- function(values, name) {
|
||||
stop("Single-bracket indexing of reactivevalues object is not allowed.")
|
||||
rlang::abort("Can't index reactivevalues with `[`.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
`[<-.reactivevalues` <- function(values, name, value) {
|
||||
stop("Single-bracket indexing of reactivevalues object is not allowed.")
|
||||
rlang::abort("Can't index reactivevalues with `[`.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
@@ -652,16 +673,15 @@ names.reactivevalues <- function(x) {
|
||||
|
||||
#' @export
|
||||
`names<-.reactivevalues` <- function(x, value) {
|
||||
stop("Can't assign names to reactivevalues object")
|
||||
rlang::abort("Can't assign names to reactivevalues.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
as.list.reactivevalues <- function(x, all.names=FALSE, ...) {
|
||||
shinyDeprecated("reactiveValuesToList",
|
||||
msg = paste("'as.list.reactivevalues' is deprecated. ",
|
||||
"Use reactiveValuesToList instead.",
|
||||
"\nPlease see ?reactiveValuesToList for more information.",
|
||||
sep = ""))
|
||||
shinyDeprecated(
|
||||
"0.4.0", "as.list.reactivevalues()", "reactiveValuesToList()",
|
||||
details = "Please see ?reactiveValuesToList for more information."
|
||||
)
|
||||
|
||||
reactiveValuesToList(x, all.names)
|
||||
}
|
||||
@@ -727,7 +747,10 @@ str.reactivevalues <- function(object, indent.str = " ", ...) {
|
||||
#' thing that happens if `req(FALSE)` is called. The value is thawed
|
||||
#' (un-frozen; accessing it will no longer raise an exception) when the current
|
||||
#' reactive domain is flushed. In a Shiny application, this occurs after all of
|
||||
#' the observers are executed.
|
||||
#' the observers are executed. **NOTE:** We are considering deprecating
|
||||
#' `freezeReactiveVal`, and `freezeReactiveValue` except when `x` is `input`.
|
||||
#' If this affects your app, please let us know by leaving a comment on
|
||||
#' [this GitHub issue](https://github.com/rstudio/shiny/issues/3063).
|
||||
#'
|
||||
#' @param x For `freezeReactiveValue`, a [reactiveValues()]
|
||||
#' object (like `input`); for `freezeReactiveVal`, a
|
||||
@@ -773,9 +796,7 @@ str.reactivevalues <- function(object, indent.str = " ", ...) {
|
||||
#' @export
|
||||
freezeReactiveValue <- function(x, name) {
|
||||
domain <- getDefaultReactiveDomain()
|
||||
if (is.null(domain)) {
|
||||
stop("freezeReactiveValue() must be called when a default reactive domain is active.")
|
||||
}
|
||||
checkReactiveDomain(domain)
|
||||
|
||||
domain$freezeValue(x, name)
|
||||
invisible()
|
||||
@@ -807,9 +828,10 @@ Observable <- R6Class(
|
||||
domain = getDefaultReactiveDomain(),
|
||||
..stacktraceon = TRUE) {
|
||||
if (length(formals(func)) > 0)
|
||||
stop("Can't make a reactive expression from a function that takes one ",
|
||||
"or more parameters; only functions without parameters can be ",
|
||||
"reactive.")
|
||||
rlang::abort(c(
|
||||
"Can't make a reactive expression from a function that takes arguments.",
|
||||
"Only functions without parameters can become reactive expressions."
|
||||
))
|
||||
|
||||
# This is to make sure that the function labels that show in the profiler
|
||||
# and in stack traces doesn't contain whitespace. See
|
||||
@@ -935,6 +957,7 @@ Observable <- R6Class(
|
||||
#' @param domain See [domains].
|
||||
#' @param ..stacktraceon Advanced use only. For stack manipulation purposes; see
|
||||
#' [stacktrace()].
|
||||
#' @param ... Not used.
|
||||
#' @return a function, wrapped in a S3 class "reactive"
|
||||
#'
|
||||
#' @examples
|
||||
@@ -956,20 +979,30 @@ Observable <- R6Class(
|
||||
#' isolate(reactiveC())
|
||||
#' isolate(reactiveD())
|
||||
#' @export
|
||||
reactive <- function(x, env = parent.frame(), quoted = FALSE, label = NULL,
|
||||
domain = getDefaultReactiveDomain(),
|
||||
..stacktraceon = TRUE) {
|
||||
fun <- exprToFunction(x, env, quoted)
|
||||
reactive <- function(x, env = parent.frame(), quoted = FALSE,
|
||||
...,
|
||||
label = NULL,
|
||||
domain = getDefaultReactiveDomain(),
|
||||
..stacktraceon = TRUE)
|
||||
{
|
||||
check_dots_empty()
|
||||
|
||||
x <- get_quosure(x, env, quoted)
|
||||
fun <- as_function(x)
|
||||
# as_function returns a function that takes `...`. We need one that takes no
|
||||
# args.
|
||||
formals(fun) <- list()
|
||||
|
||||
# Attach a label and a reference to the original user source for debugging
|
||||
srcref <- attr(substitute(x), "srcref", exact = TRUE)
|
||||
if (is.null(label)) {
|
||||
label <- rexprSrcrefToLabel(srcref[[1]],
|
||||
sprintf('reactive(%s)', paste(deparse(body(fun)), collapse='\n')))
|
||||
}
|
||||
if (length(srcref) >= 2) attr(label, "srcref") <- srcref[[2]]
|
||||
attr(label, "srcfile") <- srcFileOfRef(srcref[[1]])
|
||||
label <- exprToLabel(get_expr(x), "reactive", label)
|
||||
|
||||
o <- Observable$new(fun, label, domain, ..stacktraceon = ..stacktraceon)
|
||||
structure(o$getValue, observable = o, class = c("reactiveExpr", "reactive", "function"))
|
||||
structure(
|
||||
o$getValue,
|
||||
observable = o,
|
||||
cacheHint = list(userExpr = zap_srcref(get_expr(x))),
|
||||
class = c("reactiveExpr", "reactive", "function")
|
||||
)
|
||||
}
|
||||
|
||||
# Given the srcref to a reactive expression, attempts to figure out what the
|
||||
@@ -1038,7 +1071,15 @@ execCount <- function(x) {
|
||||
else if (inherits(x, 'Observer'))
|
||||
return(x$.execCount)
|
||||
else
|
||||
stop('Unexpected argument to execCount')
|
||||
rlang::abort("Unexpected argument to execCount().")
|
||||
}
|
||||
|
||||
# Internal utility functions for extracting things out of reactives.
|
||||
reactive_get_value_func <- function(x) {
|
||||
attr(x, "observable", exact = TRUE)$.origFunc
|
||||
}
|
||||
reactive_get_domain <- function(x) {
|
||||
attr(x, "observable", exact = TRUE)$.domain
|
||||
}
|
||||
|
||||
# Observer ------------------------------------------------------------------
|
||||
@@ -1070,8 +1111,10 @@ Observer <- R6Class(
|
||||
domain = getDefaultReactiveDomain(),
|
||||
autoDestroy = TRUE, ..stacktraceon = TRUE) {
|
||||
if (length(formals(observerFunc)) > 0)
|
||||
stop("Can't make an observer from a function that takes parameters; ",
|
||||
"only functions without parameters can be reactive.")
|
||||
rlang::abort(c(
|
||||
"Can't make an observer from a function that takes arguments.",
|
||||
"Only functions without arguments can become observers."
|
||||
))
|
||||
if (grepl("\\s", label, perl = TRUE)) {
|
||||
funcLabel <- "<observer>"
|
||||
} else {
|
||||
@@ -1301,6 +1344,8 @@ Observer <- R6Class(
|
||||
#' automatically destroyed when its domain (if any) ends.
|
||||
#' @param ..stacktraceon Advanced use only. For stack manipulation purposes; see
|
||||
#' [stacktrace()].
|
||||
#' @param ... Not used.
|
||||
#'
|
||||
#' @return An observer reference class object. This object has the following
|
||||
#' methods:
|
||||
#' \describe{
|
||||
@@ -1355,18 +1400,36 @@ Observer <- R6Class(
|
||||
#' # are at the console, you can force a flush with flushReact()
|
||||
#' shiny:::flushReact()
|
||||
#' @export
|
||||
observe <- function(x, env=parent.frame(), quoted=FALSE, label=NULL,
|
||||
suspended=FALSE, priority=0,
|
||||
domain=getDefaultReactiveDomain(), autoDestroy = TRUE,
|
||||
..stacktraceon = TRUE) {
|
||||
observe <- function(x, env = parent.frame(), quoted = FALSE,
|
||||
...,
|
||||
label = NULL,
|
||||
suspended = FALSE,
|
||||
priority = 0,
|
||||
domain = getDefaultReactiveDomain(),
|
||||
autoDestroy = TRUE,
|
||||
..stacktraceon = TRUE)
|
||||
{
|
||||
check_dots_empty()
|
||||
|
||||
fun <- exprToFunction(x, env, quoted)
|
||||
if (is.null(label))
|
||||
label <- sprintf('observe(%s)', paste(deparse(body(fun)), collapse='\n'))
|
||||
x <- get_quosure(x, env, quoted)
|
||||
fun <- as_function(x)
|
||||
# as_function returns a function that takes `...`. We need one that takes no
|
||||
# args.
|
||||
formals(fun) <- list()
|
||||
|
||||
o <- Observer$new(fun, label=label, suspended=suspended, priority=priority,
|
||||
domain=domain, autoDestroy=autoDestroy,
|
||||
..stacktraceon=..stacktraceon)
|
||||
if (is.null(label)) {
|
||||
label <- sprintf('observe(%s)', paste(deparse(get_expr(x)), collapse='\n'))
|
||||
}
|
||||
|
||||
o <- Observer$new(
|
||||
fun,
|
||||
label = label,
|
||||
suspended = suspended,
|
||||
priority = priority,
|
||||
domain = domain,
|
||||
autoDestroy = autoDestroy,
|
||||
..stacktraceon = ..stacktraceon
|
||||
)
|
||||
invisible(o)
|
||||
}
|
||||
|
||||
@@ -1377,35 +1440,34 @@ observe <- function(x, env=parent.frame(), quoted=FALSE, label=NULL,
|
||||
#' already exist; if so, its value will be used as the initial value of the
|
||||
#' reactive variable (or `NULL` if the variable did not exist).
|
||||
#'
|
||||
#' @param symbol A character string indicating the name of the variable that
|
||||
#' should be made reactive
|
||||
#' @param env The environment that will contain the reactive variable
|
||||
#'
|
||||
#' @param symbol Name of variable to make reactive, as a string.
|
||||
#' @param env Environment in which to create binding. Expert use only.
|
||||
#' @return None.
|
||||
#'
|
||||
#' @keywords internal
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' reactiveConsole(TRUE)
|
||||
#'
|
||||
#' a <- 10
|
||||
#' makeReactiveBinding("a")
|
||||
#'
|
||||
#' b <- reactive(a * -1)
|
||||
#' observe(print(b()))
|
||||
#'
|
||||
#' a <- 20
|
||||
#' }
|
||||
#' a <- 30
|
||||
#'
|
||||
#' reactiveConsole(FALSE)
|
||||
#' @export
|
||||
makeReactiveBinding <- function(symbol, env = parent.frame()) {
|
||||
if (exists(symbol, envir = env, inherits = FALSE)) {
|
||||
initialValue <- env[[symbol]]
|
||||
rm(list = symbol, envir = env, inherits = FALSE)
|
||||
}
|
||||
else
|
||||
} else {
|
||||
initialValue <- NULL
|
||||
values <- reactiveValues(value = initialValue)
|
||||
makeActiveBinding(symbol, env=env, fun=function(v) {
|
||||
if (missing(v))
|
||||
values$value
|
||||
else
|
||||
values$value <- v
|
||||
})
|
||||
}
|
||||
|
||||
val <- reactiveVal(initialValue, label = symbol)
|
||||
makeActiveBinding(symbol, val, env = env)
|
||||
|
||||
invisible()
|
||||
}
|
||||
@@ -1441,6 +1503,29 @@ setAutoflush <- local({
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
#' Activate reactivity in the console
|
||||
#'
|
||||
#' This is an experimental feature that allows you to enable reactivity
|
||||
#' at the console, for the purposes of experimentation and learning.
|
||||
#'
|
||||
#' @keywords internal
|
||||
#' @param enabled Turn console reactivity on or off?
|
||||
#' @export
|
||||
#' @examples
|
||||
#' reactiveConsole(TRUE)
|
||||
#' x <- reactiveVal(10)
|
||||
#' y <- observe({
|
||||
#' message("The value of x is ", x())
|
||||
#' })
|
||||
#' x(20)
|
||||
#' x(30)
|
||||
#' reactiveConsole(FALSE)
|
||||
reactiveConsole <- function(enabled) {
|
||||
options(shiny.suppressMissingContextError = enabled)
|
||||
setAutoflush(enabled)
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#' Timer
|
||||
@@ -1737,6 +1822,7 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
|
||||
rv <- reactiveValues(cookie = isolate(checkFunc()))
|
||||
|
||||
re_finalized <- FALSE
|
||||
env <- environment()
|
||||
|
||||
o <- observe({
|
||||
# When no one holds a reference to the reactive returned from
|
||||
@@ -1744,7 +1830,7 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
|
||||
# firing and hold onto resources.
|
||||
if (re_finalized) {
|
||||
o$destroy()
|
||||
rm(o, envir = parent.env(environment()))
|
||||
rm(o, envir = env)
|
||||
return()
|
||||
}
|
||||
|
||||
@@ -1948,7 +2034,11 @@ maskReactiveContext <- function(expr) {
|
||||
|
||||
#' Event handler
|
||||
#'
|
||||
#' Respond to "event-like" reactive inputs, values, and expressions.
|
||||
#' Respond to "event-like" reactive inputs, values, and expressions. As of Shiny
|
||||
#' 1.6.0, we recommend using [bindEvent()] instead of `eventReactive()` and
|
||||
#' `observeEvent()`. This is because `bindEvent()` can be composed with
|
||||
#' [bindCache()], and because it can also be used with `render` functions (like
|
||||
#' [renderText()] and [renderPlot()]).
|
||||
#'
|
||||
#' Shiny's reactive programming framework is primarily designed for calculated
|
||||
#' values (reactive expressions) and side-effect-causing actions (observers)
|
||||
@@ -1970,13 +2060,17 @@ maskReactiveContext <- function(expr) {
|
||||
#' response to an event. (Note that "recalculate a value" does not generally
|
||||
#' count as performing an action--see `eventReactive` for that.) The first
|
||||
#' argument is the event you want to respond to, and the second argument is a
|
||||
#' function that should be called whenever the event occurs.
|
||||
#' function that should be called whenever the event occurs. Note that
|
||||
#' `observeEvent()` is equivalent to using `observe() %>% bindEvent()` and as of
|
||||
#' Shiny 1.6.0, we recommend the latter.
|
||||
#'
|
||||
#' Use `eventReactive` to create a *calculated value* that only
|
||||
#' updates in response to an event. This is just like a normal
|
||||
#' [reactive expression][reactive] except it ignores all the usual
|
||||
#' invalidations that come from its reactive dependencies; it only invalidates
|
||||
#' in response to the given event.
|
||||
#' in response to the given event. Note that
|
||||
#' `eventReactive()` is equivalent to using `reactive() %>% bindEvent()` and as of
|
||||
#' Shiny 1.6.0, we recommend the latter.
|
||||
#'
|
||||
#' @section ignoreNULL and ignoreInit:
|
||||
#'
|
||||
@@ -2010,6 +2104,7 @@ maskReactiveContext <- function(expr) {
|
||||
#' Even though `ignoreNULL` and `ignoreInit` can be used for similar
|
||||
#' purposes they are independent from one another. Here's the result of combining
|
||||
#' these:
|
||||
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{`ignoreNULL = TRUE` and `ignoreInit = FALSE`}{
|
||||
@@ -2087,6 +2182,7 @@ maskReactiveContext <- function(expr) {
|
||||
#' after the first time that the code in `handlerExpr` is run. This
|
||||
#' pattern is useful when you want to subscribe to a event that should only
|
||||
#' happen once.
|
||||
#' @param ... Currently not used.
|
||||
#'
|
||||
#' @return `observeEvent` returns an observer reference class object (see
|
||||
#' [observe()]). `eventReactive` returns a reactive expression
|
||||
@@ -2095,7 +2191,7 @@ maskReactiveContext <- function(expr) {
|
||||
#' @seealso [actionButton()]
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' ## Only run examples in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#'
|
||||
#' ## App 1: Sample usage
|
||||
@@ -2114,6 +2210,12 @@ maskReactiveContext <- function(expr) {
|
||||
#' observeEvent(input$button, {
|
||||
#' cat("Showing", input$x, "rows\n")
|
||||
#' })
|
||||
#' # The observeEvent() above is equivalent to:
|
||||
#' # observe({
|
||||
#' # cat("Showing", input$x, "rows\n")
|
||||
#' # }) %>%
|
||||
#' # bindEvent(input$button)
|
||||
#'
|
||||
#' # Take a reactive dependency on input$button, but
|
||||
#' # not on any of the stuff inside the function
|
||||
#' df <- eventReactive(input$button, {
|
||||
@@ -2133,6 +2235,12 @@ maskReactiveContext <- function(expr) {
|
||||
#' print(paste("This will only be printed once; all",
|
||||
#' "subsequent button clicks won't do anything"))
|
||||
#' }, once = TRUE)
|
||||
#' # The observeEvent() above is equivalent to:
|
||||
#' # observe({
|
||||
#' # print(paste("This will only be printed once; all",
|
||||
#' # "subsequent button clicks won't do anything"))
|
||||
#' # }) %>%
|
||||
#' # bindEvent(input$go, once = TRUE)
|
||||
#' }
|
||||
#' )
|
||||
#'
|
||||
@@ -2159,42 +2267,38 @@ maskReactiveContext <- function(expr) {
|
||||
observeEvent <- function(eventExpr, handlerExpr,
|
||||
event.env = parent.frame(), event.quoted = FALSE,
|
||||
handler.env = parent.frame(), handler.quoted = FALSE,
|
||||
...,
|
||||
label = NULL, suspended = FALSE, priority = 0,
|
||||
domain = getDefaultReactiveDomain(), autoDestroy = TRUE,
|
||||
ignoreNULL = TRUE, ignoreInit = FALSE, once = FALSE) {
|
||||
ignoreNULL = TRUE, ignoreInit = FALSE, once = FALSE)
|
||||
{
|
||||
check_dots_empty()
|
||||
|
||||
eventFunc <- exprToFunction(eventExpr, event.env, event.quoted)
|
||||
if (is.null(label))
|
||||
label <- sprintf('observeEvent(%s)', paste(deparse(body(eventFunc)), collapse='\n'))
|
||||
eventFunc <- wrapFunctionLabel(eventFunc, "observeEventExpr", ..stacktraceon = TRUE)
|
||||
eventExpr <- get_quosure(eventExpr, event.env, event.quoted)
|
||||
handlerExpr <- get_quosure(handlerExpr, handler.env, handler.quoted)
|
||||
|
||||
handlerFunc <- exprToFunction(handlerExpr, handler.env, handler.quoted)
|
||||
handlerFunc <- wrapFunctionLabel(handlerFunc, "observeEventHandler", ..stacktraceon = TRUE)
|
||||
if (is.null(label)) {
|
||||
label <- sprintf('observeEvent(%s)', paste(deparse(get_expr(eventExpr)), collapse='\n'))
|
||||
}
|
||||
|
||||
initialized <- FALSE
|
||||
handler <- inject(observe(
|
||||
!!handlerExpr,
|
||||
label = label,
|
||||
suspended = suspended,
|
||||
priority = priority,
|
||||
domain = domain,
|
||||
autoDestroy = TRUE,
|
||||
..stacktraceon = FALSE # TODO: Does this go in the bindEvent?
|
||||
))
|
||||
|
||||
o <- observe({
|
||||
hybrid_chain(
|
||||
{eventFunc()},
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
return()
|
||||
}
|
||||
|
||||
if (ignoreNULL && isNullEvent(value)) {
|
||||
return()
|
||||
}
|
||||
|
||||
if (once) {
|
||||
on.exit(o$destroy())
|
||||
}
|
||||
|
||||
isolate(handlerFunc())
|
||||
}
|
||||
)
|
||||
}, label = label, suspended = suspended, priority = priority, domain = domain,
|
||||
autoDestroy = TRUE, ..stacktraceon = FALSE)
|
||||
o <- inject(bindEvent(
|
||||
ignoreNULL = ignoreNULL,
|
||||
ignoreInit = ignoreInit,
|
||||
once = once,
|
||||
label = label,
|
||||
!!eventExpr,
|
||||
x = handler
|
||||
))
|
||||
|
||||
invisible(o)
|
||||
}
|
||||
@@ -2204,34 +2308,26 @@ observeEvent <- function(eventExpr, handlerExpr,
|
||||
eventReactive <- function(eventExpr, valueExpr,
|
||||
event.env = parent.frame(), event.quoted = FALSE,
|
||||
value.env = parent.frame(), value.quoted = FALSE,
|
||||
...,
|
||||
label = NULL, domain = getDefaultReactiveDomain(),
|
||||
ignoreNULL = TRUE, ignoreInit = FALSE) {
|
||||
ignoreNULL = TRUE, ignoreInit = FALSE)
|
||||
{
|
||||
check_dots_empty()
|
||||
|
||||
eventFunc <- exprToFunction(eventExpr, event.env, event.quoted)
|
||||
if (is.null(label))
|
||||
label <- sprintf('eventReactive(%s)', paste(deparse(body(eventFunc)), collapse='\n'))
|
||||
eventFunc <- wrapFunctionLabel(eventFunc, "eventReactiveExpr", ..stacktraceon = TRUE)
|
||||
eventExpr <- get_quosure(eventExpr, event.env, event.quoted)
|
||||
valueExpr <- get_quosure(valueExpr, value.env, value.quoted)
|
||||
|
||||
handlerFunc <- exprToFunction(valueExpr, value.env, value.quoted)
|
||||
handlerFunc <- wrapFunctionLabel(handlerFunc, "eventReactiveHandler", ..stacktraceon = TRUE)
|
||||
if (is.null(label)) {
|
||||
label <- sprintf('eventReactive(%s)', paste(deparse(get_expr(eventExpr)), collapse='\n'))
|
||||
}
|
||||
|
||||
initialized <- FALSE
|
||||
|
||||
invisible(reactive({
|
||||
hybrid_chain(
|
||||
eventFunc(),
|
||||
function(value) {
|
||||
if (ignoreInit && !initialized) {
|
||||
initialized <<- TRUE
|
||||
req(FALSE)
|
||||
}
|
||||
|
||||
req(!ignoreNULL || !isNullEvent(value))
|
||||
|
||||
isolate(handlerFunc())
|
||||
}
|
||||
)
|
||||
}, label = label, domain = domain, ..stacktraceon = FALSE))
|
||||
invisible(inject(bindEvent(
|
||||
ignoreNULL = ignoreNULL,
|
||||
ignoreInit = ignoreInit,
|
||||
label = label,
|
||||
!!eventExpr,
|
||||
x = reactive(!!valueExpr, domain = domain, label = label)
|
||||
)))
|
||||
}
|
||||
|
||||
isNullEvent <- function(value) {
|
||||
@@ -2391,7 +2487,7 @@ debounce <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
|
||||
now <- getDomainTimeMs(domain)
|
||||
if (now >= v$when) {
|
||||
# Mod by 999999999 to get predictable overflow behavior
|
||||
v$trigger <- isolate(v$trigger %OR% 0) %% 999999999 + 1
|
||||
v$trigger <- isolate(v$trigger %||% 0) %% 999999999 + 1
|
||||
v$when <- NULL
|
||||
} else {
|
||||
invalidateLater(v$when - now)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#' Plot output with cached images
|
||||
#'
|
||||
#' Renders a reactive plot, with plot images cached to disk.
|
||||
#' Renders a reactive plot, with plot images cached to disk. As of Shiny 1.6.0,
|
||||
#' this is a shortcut for using [bindCache()] with [renderPlot()].
|
||||
#'
|
||||
#' `expr` is an expression that generates a plot, similar to that in
|
||||
#' `renderPlot`. Unlike with `renderPlot`, this expression does not
|
||||
@@ -8,7 +9,7 @@
|
||||
#' changes.
|
||||
#'
|
||||
#' `cacheKeyExpr` is an expression which, when evaluated, returns an object
|
||||
#' which will be serialized and hashed using the [digest::digest()]
|
||||
#' which will be serialized and hashed using the [rlang::hash()]
|
||||
#' function to generate a string that will be used as a cache key. This key is
|
||||
#' used to identify the contents of the plot: if the cache key is the same as a
|
||||
#' previous time, it assumes that the plot is the same and can be retrieved from
|
||||
@@ -32,7 +33,7 @@
|
||||
#' to normal R objects before returning them. Your expression could even
|
||||
#' serialize and hash that information in an efficient way and return a string,
|
||||
#' which will in turn be hashed (very quickly) by the
|
||||
#' [digest::digest()] function.
|
||||
#' [rlang::hash()] function.
|
||||
#'
|
||||
#' Internally, the result from `cacheKeyExpr` is combined with the name of
|
||||
#' the output (if you assign it to `output$plot1`, it will be combined
|
||||
@@ -40,95 +41,6 @@
|
||||
#' if there are multiple plots that have the same `cacheKeyExpr`, they
|
||||
#' will not have cache key collisions.
|
||||
#'
|
||||
#' @section Cache scoping:
|
||||
#'
|
||||
#' There are a number of different ways you may want to scope the cache. For
|
||||
#' example, you may want each user session to have their own plot cache, or
|
||||
#' you may want each run of the application to have a cache (shared among
|
||||
#' possibly multiple simultaneous user sessions), or you may want to have a
|
||||
#' cache that persists even after the application is shut down and started
|
||||
#' again.
|
||||
#'
|
||||
#' To control the scope of the cache, use the `cache` parameter. There
|
||||
#' are two ways of having Shiny automatically create and clean up the disk
|
||||
#' cache.
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{1}{To scope the cache to one run of a Shiny application (shared
|
||||
#' among possibly multiple user sessions), use `cache="app"`. This
|
||||
#' is the default. The cache will be shared across multiple sessions, so
|
||||
#' there is potentially a large performance benefit if there are many users
|
||||
#' of the application. When the application stops running, the cache will
|
||||
#' be deleted. If plots cannot be safely shared across users, this should
|
||||
#' not be used.}
|
||||
#' \item{2}{To scope the cache to one session, use `cache="session"`.
|
||||
#' When a new user session starts --- in other words, when a web browser
|
||||
#' visits the Shiny application --- a new cache will be created on disk
|
||||
#' for that session. When the session ends, the cache will be deleted.
|
||||
#' The cache will not be shared across multiple sessions.}
|
||||
#' }
|
||||
#'
|
||||
#' If either `"app"` or `"session"` is used, the cache will be 10 MB
|
||||
#' in size, and will be stored stored in memory, using a
|
||||
#' [memoryCache()] object. Note that the cache space will be shared
|
||||
#' among all cached plots within a single application or session.
|
||||
#'
|
||||
#' In some cases, you may want more control over the caching behavior. For
|
||||
#' example, you may want to use a larger or smaller cache, share a cache
|
||||
#' among multiple R processes, or you may want the cache to persist across
|
||||
#' multiple runs of an application, or even across multiple R processes.
|
||||
#'
|
||||
#' To use different settings for an application-scoped cache, you can call
|
||||
#' [shinyOptions()] at the top of your app.R, server.R, or
|
||||
#' global.R. For example, this will create a cache with 20 MB of space
|
||||
#' instead of the default 10 MB:
|
||||
#' \preformatted{
|
||||
#' shinyOptions(cache = memoryCache(size = 20e6))
|
||||
#' }
|
||||
#'
|
||||
#' To use different settings for a session-scoped cache, you can call
|
||||
#' [shinyOptions()] at the top of your server function. To use
|
||||
#' the session-scoped cache, you must also call `renderCachedPlot` with
|
||||
#' `cache="session"`. This will create a 20 MB cache for the session:
|
||||
#' \preformatted{
|
||||
#' function(input, output, session) {
|
||||
#' shinyOptions(cache = memoryCache(size = 20e6))
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' ...,
|
||||
#' cache = "session"
|
||||
#' )
|
||||
#' }
|
||||
#' }
|
||||
#'
|
||||
#' If you want to create a cache that is shared across multiple concurrent
|
||||
#' R processes, you can use a [diskCache()]. You can create an
|
||||
#' application-level shared cache by putting this at the top of your app.R,
|
||||
#' server.R, or global.R:
|
||||
#' \preformatted{
|
||||
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
#' }
|
||||
#'
|
||||
#' This will create a subdirectory in your system temp directory named
|
||||
#' `myapp-cache` (replace `myapp-cache` with a unique name of
|
||||
#' your choosing). On most platforms, this directory will be removed when
|
||||
#' your system reboots. This cache will persist across multiple starts and
|
||||
#' stops of the R process, as long as you do not reboot.
|
||||
#'
|
||||
#' To have the cache persist even across multiple reboots, you can create the
|
||||
#' cache in a location outside of the temp directory. For example, it could
|
||||
#' be a subdirectory of the application:
|
||||
#' \preformatted{
|
||||
#' shinyOptions(cache = diskCache("./myapp-cache"))
|
||||
#' }
|
||||
#'
|
||||
#' In this case, resetting the cache will have to be done manually, by deleting
|
||||
#' the directory.
|
||||
#'
|
||||
#' You can also scope a cache to just one plot, or selected plots. To do that,
|
||||
#' create a [memoryCache()] or [diskCache()], and pass it
|
||||
#' as the `cache` argument of `renderCachedPlot`.
|
||||
#'
|
||||
#' @section Interactive plots:
|
||||
#'
|
||||
#' `renderCachedPlot` can be used to create interactive plots. See
|
||||
@@ -136,6 +48,7 @@
|
||||
#'
|
||||
#'
|
||||
#' @inheritParams renderPlot
|
||||
#' @inheritParams bindCache
|
||||
#' @param cacheKeyExpr An expression that returns a cache key. This key should
|
||||
#' be a unique identifier for a plot: the assumption is that if the cache key
|
||||
#' is the same, then the plot will be the same.
|
||||
@@ -146,16 +59,13 @@
|
||||
#' possible pixel dimension. See [sizeGrowthRatio()] for more
|
||||
#' information on the default sizing policy.
|
||||
#' @param res The resolution of the PNG, in pixels per inch.
|
||||
#' @param cache The scope of the cache, or a cache object. This can be
|
||||
#' `"app"` (the default), `"session"`, or a cache object like
|
||||
#' a [diskCache()]. See the Cache Scoping section for more
|
||||
#' information.
|
||||
#' @param width,height not used. They are specified via the argument
|
||||
#' `sizePolicy`.
|
||||
#'
|
||||
#' @seealso See [renderPlot()] for the regular, non-cached version of
|
||||
#' this function. For more about configuring caches, see
|
||||
#' [memoryCache()] and [diskCache()].
|
||||
#' @seealso See [renderPlot()] for the regular, non-cached version of this
|
||||
#' function. It can be used with [bindCache()] to get the same effect as
|
||||
#' `renderCachedPlot()`. For more about configuring caches, see
|
||||
#' [cachem::cache_mem()] and [cachem::cache_disk()].
|
||||
#'
|
||||
#'
|
||||
#' @examples
|
||||
@@ -246,7 +156,7 @@
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) },
|
||||
#' cache = memoryCache()
|
||||
#' cache = cachem::cache_mem()
|
||||
#' )
|
||||
#' output$plot2 <- renderCachedPlot({
|
||||
#' Sys.sleep(2) # Add an artificial delay
|
||||
@@ -255,7 +165,7 @@
|
||||
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
|
||||
#' },
|
||||
#' cacheKeyExpr = { list(input$n) },
|
||||
#' cache = memoryCache()
|
||||
#' cache = cachem::cache_mem()
|
||||
#' )
|
||||
#' }
|
||||
#' )
|
||||
@@ -266,22 +176,22 @@
|
||||
#' # At the top of app.R, this set the application-scoped cache to be a memory
|
||||
#' # cache that is 20 MB in size, and where cached objects expire after one
|
||||
#' # hour.
|
||||
#' shinyOptions(cache = memoryCache(max_size = 20e6, max_age = 3600))
|
||||
#' shinyOptions(cache = cachem::cache_mem(max_size = 20e6, max_age = 3600))
|
||||
#'
|
||||
#' # 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
|
||||
#' # deleted when the system reboots.
|
||||
#' shinyOptions(cache = diskCache(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
|
||||
#' # cache that can be shared among multiple concurrent R processes, and
|
||||
#' # persists on disk across reboots.
|
||||
#' shinyOptions(cache = diskCache("./myapp-cache"))
|
||||
#' shinyOptions(cache = cachem::cache_disk("./myapp-cache"))
|
||||
#'
|
||||
#' # At the top of the server function, this set the session-scoped cache to be
|
||||
#' # a memory cache that is 5 MB in size.
|
||||
#' server <- function(input, output, session) {
|
||||
#' shinyOptions(cache = memoryCache(max_size = 5e6))
|
||||
#' shinyOptions(cache = cachem::cache_mem(max_size = 5e6))
|
||||
#'
|
||||
#' output$plot <- renderCachedPlot(
|
||||
#' ...,
|
||||
@@ -303,275 +213,29 @@ renderCachedPlot <- function(expr,
|
||||
height = NULL
|
||||
) {
|
||||
|
||||
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
|
||||
# is called
|
||||
installExprFunction(expr, "func", parent.frame(), quoted = FALSE, ..stacktraceon = TRUE)
|
||||
# This is so that the expr doesn't re-execute by itself; it needs to be
|
||||
# triggered by the cache key (or width/height) changing.
|
||||
isolatedFunc <- function() isolate(func())
|
||||
expr <- substitute(expr)
|
||||
if (!is_quosure(expr)) {
|
||||
expr <- new_quosure(expr, env = parent.frame())
|
||||
}
|
||||
|
||||
args <- list(...)
|
||||
cacheKeyExpr <- substitute(cacheKeyExpr)
|
||||
if (!is_quosure(cacheKeyExpr)) {
|
||||
cacheKeyExpr <- new_quosure(cacheKeyExpr, env = parent.frame())
|
||||
}
|
||||
|
||||
if (!is.null(width) || !is.null(height)) {
|
||||
warning("Unused argument(s) 'width' and/or 'height'. ",
|
||||
"'sizePolicy' is used instead.")
|
||||
}
|
||||
|
||||
cacheKeyExpr <- substitute(cacheKeyExpr)
|
||||
# The real cache key we'll use also includes width, height, res, pixelratio.
|
||||
# This is just the part supplied by the user.
|
||||
userCacheKey <- reactive(cacheKeyExpr, env = parent.frame(), quoted = TRUE, label = "userCacheKey")
|
||||
|
||||
ensureCacheSetup <- function() {
|
||||
# For our purposes, cache objects must support these methods.
|
||||
isCacheObject <- function(x) {
|
||||
# Use tryCatch in case the object does not support `$`.
|
||||
tryCatch(
|
||||
is.function(x$get) && is.function(x$set),
|
||||
error = function(e) FALSE
|
||||
)
|
||||
}
|
||||
|
||||
if (isCacheObject(cache)) {
|
||||
# If `cache` is already a cache object, do nothing
|
||||
return()
|
||||
|
||||
} else if (identical(cache, "app")) {
|
||||
cache <<- getShinyOption("cache")
|
||||
|
||||
} else if (identical(cache, "session")) {
|
||||
cache <<- session$cache
|
||||
|
||||
} else {
|
||||
stop('`cache` must either be "app", "session", or a cache object with methods, `$get`, and `$set`.')
|
||||
}
|
||||
}
|
||||
|
||||
# The width and height of the plot to draw, given from sizePolicy. These
|
||||
# values get filled by an observer below.
|
||||
fitDims <- reactiveValues(width = NULL, height = NULL)
|
||||
|
||||
# Make sure alt param to be reactive function
|
||||
if (is.reactive(alt))
|
||||
altWrapper <- alt
|
||||
else if (is.function(alt))
|
||||
altWrapper <- reactive({ alt() })
|
||||
else
|
||||
altWrapper <- function() { alt }
|
||||
|
||||
resizeObserver <- NULL
|
||||
ensureResizeObserver <- function() {
|
||||
if (!is.null(resizeObserver))
|
||||
return()
|
||||
|
||||
# Given the actual width/height of the image in the browser, this gets the
|
||||
# width/height from sizePolicy() and pushes those values into `fitDims`.
|
||||
# It's done this way so that the `fitDims` only change (and cause
|
||||
# invalidations) when the rendered image size changes, and not every time
|
||||
# the browser's <img> tag changes size.
|
||||
doResizeCheck <- function() {
|
||||
width <- session$clientData[[paste0('output_', outputName, '_width')]]
|
||||
height <- session$clientData[[paste0('output_', outputName, '_height')]]
|
||||
|
||||
if (is.null(width)) width <- 0
|
||||
if (is.null(height)) height <- 0
|
||||
|
||||
rect <- sizePolicy(c(width, height))
|
||||
fitDims$width <- rect[1]
|
||||
fitDims$height <- rect[2]
|
||||
}
|
||||
|
||||
# Run it once immediately, then set up the observer
|
||||
isolate(doResizeCheck())
|
||||
|
||||
resizeObserver <<- observe(doResizeCheck())
|
||||
}
|
||||
|
||||
# Vars to store session and output, so that they can be accessed from
|
||||
# the plotObj() reactive.
|
||||
session <- NULL
|
||||
outputName <- NULL
|
||||
|
||||
|
||||
drawReactive <- reactive(label = "plotObj", {
|
||||
hybrid_chain(
|
||||
# Depend on the user cache key, even though we don't use the value. When
|
||||
# it changes, it can cause the drawReactive to re-execute. (Though
|
||||
# drawReactive will not necessarily re-execute --- it must be called from
|
||||
# renderFunc, which happens only if there's a cache miss.)
|
||||
userCacheKey(),
|
||||
function(userCacheKeyValue) {
|
||||
# Get width/height, but don't depend on them.
|
||||
isolate({
|
||||
width <- fitDims$width
|
||||
height <- fitDims$height
|
||||
# Make sure alt text to be reactive function
|
||||
alt <- altWrapper()
|
||||
})
|
||||
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
|
||||
do.call("drawPlot", c(
|
||||
list(
|
||||
name = outputName,
|
||||
session = session,
|
||||
func = isolatedFunc,
|
||||
width = width,
|
||||
height = height,
|
||||
alt = alt,
|
||||
pixelratio = pixelratio,
|
||||
res = res
|
||||
),
|
||||
args
|
||||
))
|
||||
},
|
||||
catch = function(reason) {
|
||||
# Non-isolating read. A common reason for errors in plotting is because
|
||||
# the dimensions are too small. By taking a dependency on width/height,
|
||||
# we can try again if the plot output element changes size.
|
||||
fitDims$width
|
||||
fitDims$height
|
||||
|
||||
# Propagate the error
|
||||
stop(reason)
|
||||
}
|
||||
inject(
|
||||
bindCache(
|
||||
renderPlot(!!expr, res = res, alt = alt, outputArgs = outputArgs, ...),
|
||||
!!cacheKeyExpr,
|
||||
sizePolicy = sizePolicy,
|
||||
cache = cache
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
# This function is the one that's returned from renderPlot(), and gets
|
||||
# wrapped in an observer when the output value is assigned.
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
outputName <<- name
|
||||
session <<- shinysession
|
||||
ensureCacheSetup()
|
||||
ensureResizeObserver()
|
||||
|
||||
hybrid_chain(
|
||||
# This use of the userCacheKey() sets up the reactive dependency that
|
||||
# causes plot re-draw events. These may involve pulling from the cache,
|
||||
# replaying a display list, or re-executing user code.
|
||||
userCacheKey(),
|
||||
function(userCacheKeyResult) {
|
||||
width <- fitDims$width
|
||||
height <- fitDims$height
|
||||
alt <- altWrapper()
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
|
||||
key <- digest::digest(list(outputName, userCacheKeyResult, width, height, res, pixelratio), "xxhash64")
|
||||
|
||||
plotObj <- cache$get(key)
|
||||
|
||||
# First look in cache.
|
||||
# Case 1. cache hit.
|
||||
if (!is.key_missing(plotObj)) {
|
||||
return(list(
|
||||
cacheHit = TRUE,
|
||||
key = key,
|
||||
plotObj = plotObj,
|
||||
width = width,
|
||||
height = height,
|
||||
alt = alt,
|
||||
pixelratio = pixelratio
|
||||
))
|
||||
}
|
||||
|
||||
# If not in cache, hybrid_chain call to drawReactive
|
||||
#
|
||||
# Two more possible cases:
|
||||
# 2. drawReactive will re-execute and return a plot that's the
|
||||
# correct size.
|
||||
# 3. It will not re-execute, but it will return the previous value,
|
||||
# which is the wrong size. It will include a valid display list
|
||||
# which can be used by resizeSavedPlot.
|
||||
hybrid_chain(
|
||||
drawReactive(),
|
||||
function(drawReactiveResult) {
|
||||
# Pass along the key for caching in the next stage
|
||||
list(
|
||||
cacheHit = FALSE,
|
||||
key = key,
|
||||
plotObj = drawReactiveResult,
|
||||
width = width,
|
||||
height = height,
|
||||
alt = alt,
|
||||
pixelratio = pixelratio
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
function(possiblyAsyncResult) {
|
||||
hybrid_chain(possiblyAsyncResult, function(result) {
|
||||
width <- result$width
|
||||
height <- result$height
|
||||
alt <- result$alt
|
||||
pixelratio <- result$pixelratio
|
||||
|
||||
# Three possibilities when we get here:
|
||||
# 1. There was a cache hit. No need to set a value in the cache.
|
||||
# 2. There was a cache miss, and the plotObj is already the correct
|
||||
# size (because drawReactive re-executed). In this case, we need
|
||||
# to cache it.
|
||||
# 3. There was a cache miss, and the plotObj was not the corect size.
|
||||
# In this case, we need to replay the display list, and then cache
|
||||
# the result.
|
||||
if (!result$cacheHit) {
|
||||
# If the image is already the correct size, this just returns the
|
||||
# object unchanged.
|
||||
result$plotObj <- do.call("resizeSavedPlot", c(
|
||||
list(
|
||||
name,
|
||||
shinysession,
|
||||
result$plotObj,
|
||||
width,
|
||||
height,
|
||||
alt,
|
||||
pixelratio,
|
||||
res
|
||||
),
|
||||
args
|
||||
))
|
||||
|
||||
# Save a cached copy of the plotObj. The recorded displaylist for
|
||||
# the plot can't be serialized and restored properly within the same
|
||||
# R session, so we NULL it out before saving. (The image data and
|
||||
# other metadata be saved and restored just fine.) Displaylists can
|
||||
# also be very large (~1.5MB for a basic ggplot), and they would not
|
||||
# be commonly used. Note that displaylist serialization was fixed in
|
||||
# revision 74506 (2e6c669), and should be in R 3.6. A MemoryCache
|
||||
# doesn't need to serialize objects, so it could actually save a
|
||||
# display list, but for the reasons listed previously, it's
|
||||
# generally not worth it.
|
||||
# The plotResult is not the same as the recordedPlot (it is used to
|
||||
# retrieve coordmap information for ggplot2 objects) but it is only
|
||||
# used in conjunction with the recordedPlot, and we'll remove it
|
||||
# because it can be quite large.
|
||||
result$plotObj$plotResult <- NULL
|
||||
result$plotObj$recordedPlot <- NULL
|
||||
cache$set(result$key, result$plotObj)
|
||||
}
|
||||
|
||||
img <- result$plotObj$img
|
||||
# Replace exact pixel dimensions; instead, the max-height and
|
||||
# max-width will be set to 100% from CSS.
|
||||
img$class <- "shiny-scalable"
|
||||
img$width <- NULL
|
||||
img$height <- NULL
|
||||
|
||||
img
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# If renderPlot isn't going to adapt to the height of the div, then the
|
||||
# div needs to adapt to the height of renderPlot. By default, plotOutput
|
||||
# sets the height to 400px, so to make it adapt we need to override it
|
||||
# with NULL.
|
||||
outputFunc <- plotOutput
|
||||
formals(outputFunc)['height'] <- list(NULL)
|
||||
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -62,9 +62,11 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
|
||||
env = parent.frame(), quoted = FALSE,
|
||||
execOnResize = FALSE, outputArgs = list()
|
||||
) {
|
||||
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
|
||||
# is called
|
||||
installExprFunction(expr, "func", env, quoted, ..stacktraceon = TRUE)
|
||||
func <- quoToFunction(expr, "renderPlot", ..stacktraceon = TRUE)
|
||||
|
||||
args <- list(...)
|
||||
|
||||
@@ -89,7 +91,9 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
|
||||
else
|
||||
altWrapper <- function() { alt }
|
||||
|
||||
getDims <- function() {
|
||||
# This is the function that will be used as getDims by default, but it can be
|
||||
# overridden (which happens when bindCache() is used).
|
||||
getDimsDefault <- function() {
|
||||
width <- widthWrapper()
|
||||
height <- heightWrapper()
|
||||
|
||||
@@ -108,6 +112,7 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
|
||||
# the plotObj() reactive.
|
||||
session <- NULL
|
||||
outputName <- NULL
|
||||
getDims <- NULL
|
||||
|
||||
# Calls drawPlot, invoking the user-provided `func` (which may or may not
|
||||
# return a promise). The idea is that the (cached) return value from this
|
||||
@@ -118,7 +123,7 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
|
||||
{
|
||||
# If !execOnResize, don't invalidate when width/height changes.
|
||||
dims <- if (execOnResize) getDims() else isolate(getDims())
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
pixelratio <- session$clientData$pixelratio %||% 1
|
||||
do.call("drawPlot", c(
|
||||
list(
|
||||
name = outputName,
|
||||
@@ -145,15 +150,19 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
|
||||
|
||||
# This function is the one that's returned from renderPlot(), and gets
|
||||
# wrapped in an observer when the output value is assigned.
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
# The `get_dims` parameter defaults to `getDimsDefault`. However, it can be
|
||||
# overridden, so that `bindCache` can use a different version.
|
||||
renderFunc <- function(shinysession, name, ..., get_dims = getDimsDefault) {
|
||||
|
||||
outputName <<- name
|
||||
session <<- shinysession
|
||||
if (is.null(getDims)) getDims <<- get_dims
|
||||
|
||||
hybrid_chain(
|
||||
drawReactive(),
|
||||
function(result) {
|
||||
dims <- getDims()
|
||||
pixelratio <- session$clientData$pixelratio %OR% 1
|
||||
pixelratio <- session$clientData$pixelratio %||% 1
|
||||
result <- do.call("resizeSavedPlot", c(
|
||||
list(name, shinysession, result, dims$width, dims$height, altWrapper(), pixelratio, res),
|
||||
args
|
||||
@@ -171,7 +180,14 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
|
||||
outputFunc <- plotOutput
|
||||
if (!identical(height, 'auto')) formals(outputFunc)['height'] <- list(NULL)
|
||||
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
|
||||
markedFunc <- markRenderFunction(
|
||||
outputFunc,
|
||||
renderFunc,
|
||||
outputArgs,
|
||||
cacheHint = list(userExpr = get_expr(expr), res = res)
|
||||
)
|
||||
class(markedFunc) <- c("shiny.renderPlot", class(markedFunc))
|
||||
markedFunc
|
||||
}
|
||||
|
||||
resizeSavedPlot <- function(name, session, result, width, height, alt, pixelratio, res, ...) {
|
||||
@@ -237,8 +253,9 @@ drawPlot <- function(name, session, func, width, height, alt, pixelratio, res, .
|
||||
promises::with_promise_domain(domain, {
|
||||
hybrid_chain(
|
||||
func(),
|
||||
function(value, .visible) {
|
||||
if (.visible) {
|
||||
function(value) {
|
||||
res <- withVisible(value)
|
||||
if (res$visible) {
|
||||
# A modified version of print.ggplot which returns the built ggplot object
|
||||
# as well as the gtable grob. This overrides the ggplot::print.ggplot
|
||||
# method, but only within the context of renderPlot. The reason this needs
|
||||
@@ -256,7 +273,7 @@ drawPlot <- function(name, session, func, width, height, alt, pixelratio, res, .
|
||||
# similar to ggplot2. But for base graphics, it would already have
|
||||
# been rendered when func was called above, and the print should
|
||||
# have no effect.
|
||||
result <- ..stacktraceon..(print(value))
|
||||
result <- ..stacktraceon..(print(res$value))
|
||||
# TODO jcheng 2017-04-11: Verify above ..stacktraceon..
|
||||
})
|
||||
result
|
||||
@@ -593,6 +610,10 @@ find_panel_info_api <- function(b) {
|
||||
coord <- ggplot2::summarise_coord(b)
|
||||
layers <- ggplot2::summarise_layers(b)
|
||||
|
||||
`%NA_OR%` <- function(x, y) {
|
||||
if (is_na(x)) y else x
|
||||
}
|
||||
|
||||
# Given x and y scale objects and a coord object, return a list that has
|
||||
# the bases of log transformations for x and y, or NULL if it's not a
|
||||
# log transform.
|
||||
@@ -609,8 +630,8 @@ find_panel_info_api <- function(b) {
|
||||
|
||||
# First look for log base in scale, then coord; otherwise NULL.
|
||||
list(
|
||||
x = get_log_base(xscale$trans) %OR% coord$xlog %OR% NULL,
|
||||
y = get_log_base(yscale$trans) %OR% coord$ylog %OR% NULL
|
||||
x = get_log_base(xscale$trans) %NA_OR% coord$xlog %NA_OR% NULL,
|
||||
y = get_log_base(yscale$trans) %NA_OR% coord$ylog %NA_OR% NULL
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#' Table Output
|
||||
#'
|
||||
#' Creates a reactive table that is suitable for assigning to an `output`
|
||||
#' slot.
|
||||
#' @description
|
||||
#' The `tableOuptut()`/`renderTable()` pair creates a reactive table that is
|
||||
#' suitable for display small matrices and data frames. The columns are
|
||||
#' formatted with [xtable::xtable()].
|
||||
#'
|
||||
#' The corresponding HTML output tag should be `div` and have the CSS
|
||||
#' class name `shiny-html-output`.
|
||||
#' See [renderDataTable()] for data frames that are too big to fit on a single
|
||||
#' page.
|
||||
#'
|
||||
#' @param expr An expression that returns an R object that can be used with
|
||||
#' [xtable::xtable()].
|
||||
@@ -47,14 +49,33 @@
|
||||
#' implicit call to [tableOutput()] when `renderTable` is
|
||||
#' used in an interactive R Markdown document.
|
||||
#' @export
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' # table example
|
||||
#' shinyApp(
|
||||
#' ui = fluidPage(
|
||||
#' fluidRow(
|
||||
#' column(12,
|
||||
#' tableOutput('table')
|
||||
#' )
|
||||
#' )
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$table <- renderTable(iris)
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
renderTable <- function(expr, striped = FALSE, hover = FALSE,
|
||||
bordered = FALSE, spacing = c("s", "xs", "m", "l"),
|
||||
width = "auto", align = NULL,
|
||||
rownames = FALSE, colnames = TRUE,
|
||||
digits = NULL, na = "NA", ...,
|
||||
env = parent.frame(), quoted = FALSE,
|
||||
outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
outputArgs=list())
|
||||
{
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
func <- quoToFunction(expr, "renderTable")
|
||||
|
||||
if (!is.function(spacing)) spacing <- match.arg(spacing)
|
||||
|
||||
|
||||
562
R/runapp.R
Normal file
562
R/runapp.R
Normal file
@@ -0,0 +1,562 @@
|
||||
#' Run Shiny Application
|
||||
#'
|
||||
#' Runs a Shiny application. This function normally does not return; interrupt R
|
||||
#' to stop the application (usually by pressing Ctrl+C or Esc).
|
||||
#'
|
||||
#' The host parameter was introduced in Shiny 0.9.0. Its default value of
|
||||
#' `"127.0.0.1"` means that, contrary to previous versions of Shiny, only
|
||||
#' the current machine can access locally hosted Shiny apps. To allow other
|
||||
#' clients to connect, use the value `"0.0.0.0"` instead (which was the
|
||||
#' value that was hard-coded into Shiny in 0.8.0 and earlier).
|
||||
#'
|
||||
#' @param appDir The application to run. Should be one of the following:
|
||||
#' \itemize{
|
||||
#' \item A directory containing `server.R`, plus, either `ui.R` or
|
||||
#' a `www` directory that contains the file `index.html`.
|
||||
#' \item A directory containing `app.R`.
|
||||
#' \item An `.R` file containing a Shiny application, ending with an
|
||||
#' expression that produces a Shiny app object.
|
||||
#' \item A list with `ui` and `server` components.
|
||||
#' \item A Shiny app object created by [shinyApp()].
|
||||
#' }
|
||||
#' @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
|
||||
#' `options(shiny.port = XX)`), then that port will be used. Otherwise,
|
||||
#' use a random port.
|
||||
#' @param launch.browser If true, the system's default web browser will be
|
||||
#' launched automatically after the app is started. Defaults to true in
|
||||
#' interactive sessions only. This value of this parameter can also be a
|
||||
#' function to call with the application's URL.
|
||||
#' @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
|
||||
#' Details.
|
||||
#' @param workerId Can generally be ignored. Exists to help some editions of
|
||||
#' Shiny Server Pro route requests to the correct process.
|
||||
#' @param quiet Should Shiny status messages be shown? Defaults to FALSE.
|
||||
#' @param display.mode The mode in which to display the application. If set to
|
||||
#' the value `"showcase"`, shows application code and metadata from a
|
||||
#' `DESCRIPTION` file in the application directory alongside the
|
||||
#' application. If set to `"normal"`, displays the application normally.
|
||||
#' Defaults to `"auto"`, which displays the application in the mode given
|
||||
#' in its `DESCRIPTION` file, if any.
|
||||
#' @param test.mode Should the application be launched in test mode? This is
|
||||
#' only used for recording or running automated tests. Defaults to the
|
||||
#' `shiny.testmode` option, or FALSE if the option is not set.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # Start app in the current working directory
|
||||
#' runApp()
|
||||
#'
|
||||
#' # Start app in a subdirectory called myapp
|
||||
#' runApp("myapp")
|
||||
#' }
|
||||
#'
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' # Apps can be run without a server.r and ui.r file
|
||||
#' runApp(list(
|
||||
#' ui = bootstrapPage(
|
||||
#' numericInput('n', 'Number of obs', 100),
|
||||
#' plotOutput('plot')
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$plot <- renderPlot({ hist(runif(input$n)) })
|
||||
#' }
|
||||
#' ))
|
||||
#'
|
||||
#'
|
||||
#' # Running a Shiny app object
|
||||
#' app <- shinyApp(
|
||||
#' ui = bootstrapPage(
|
||||
#' numericInput('n', 'Number of obs', 100),
|
||||
#' plotOutput('plot')
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$plot <- renderPlot({ hist(runif(input$n)) })
|
||||
#' }
|
||||
#' )
|
||||
#' runApp(app)
|
||||
#' }
|
||||
#' @export
|
||||
runApp <- function(appDir=getwd(),
|
||||
port=getOption('shiny.port'),
|
||||
launch.browser = getOption('shiny.launch.browser', interactive()),
|
||||
host=getOption('shiny.host', '127.0.0.1'),
|
||||
workerId="", quiet=FALSE,
|
||||
display.mode=c("auto", "normal", "showcase"),
|
||||
test.mode=getOption('shiny.testmode', FALSE)) {
|
||||
on.exit({
|
||||
handlerManager$clear()
|
||||
}, add = TRUE)
|
||||
|
||||
if (isRunning()) {
|
||||
stop("Can't call `runApp()` from within `runApp()`. If your ",
|
||||
"application code contains `runApp()`, please remove it.")
|
||||
}
|
||||
|
||||
# Make warnings print immediately
|
||||
# Set pool.scheduler to support pool package
|
||||
ops <- options(
|
||||
# Raise warn level to 1, but don't lower it
|
||||
warn = max(1, getOption("warn", default = 1)),
|
||||
pool.scheduler = scheduleTask
|
||||
)
|
||||
on.exit(options(ops), add = TRUE)
|
||||
|
||||
# ============================================================================
|
||||
# Global onStart/onStop callbacks
|
||||
# ============================================================================
|
||||
# Invoke user-defined onStop callbacks, before the application's internal
|
||||
# onStop callbacks.
|
||||
on.exit({
|
||||
.globals$onStopCallbacks$invoke()
|
||||
.globals$onStopCallbacks <- Callbacks$new()
|
||||
}, add = TRUE)
|
||||
|
||||
require(shiny)
|
||||
|
||||
# ============================================================================
|
||||
# Convert to Shiny app object
|
||||
# ============================================================================
|
||||
appParts <- as.shiny.appobj(appDir)
|
||||
|
||||
# ============================================================================
|
||||
# Initialize app state object
|
||||
# ============================================================================
|
||||
# This is so calls to getCurrentAppState() can be used to find (A) whether an
|
||||
# app is running and (B), get options and data associated with the app.
|
||||
initCurrentAppState(appParts)
|
||||
on.exit(clearCurrentAppState(), add = TRUE)
|
||||
# Any shinyOptions set after this point will apply to the current app only
|
||||
# (and will not persist after the app stops).
|
||||
|
||||
# ============================================================================
|
||||
# shinyOptions
|
||||
# ============================================================================
|
||||
# A unique identifier associated with this run of this application. It is
|
||||
# shared across sessions.
|
||||
shinyOptions(appToken = createUniqueId(8))
|
||||
|
||||
# Set up default cache for app.
|
||||
if (is.null(getShinyOption("cache", default = NULL))) {
|
||||
shinyOptions(cache = cachem::cache_mem(max_size = 200 * 1024^2))
|
||||
}
|
||||
|
||||
# Extract appOptions (which is a list) and store them as shinyOptions, for
|
||||
# this app. (This is the only place we have to store settings that are
|
||||
# accessible both the UI and server portion of the app.)
|
||||
applyCapturedAppOptions(appParts$appOptions)
|
||||
|
||||
# ============================================================================
|
||||
# runApp options set via shinyApp(options = list(...))
|
||||
# ============================================================================
|
||||
# The lines below set some of the app's running options, which
|
||||
# can be:
|
||||
# - left unspecified (in which case the arguments' default
|
||||
# values from `runApp` kick in);
|
||||
# - passed through `shinyApp`
|
||||
# - passed through `runApp` (this function)
|
||||
# - passed through both `shinyApp` and `runApp` (the latter
|
||||
# takes precedence)
|
||||
#
|
||||
# Matrix of possibilities:
|
||||
# | IN shinyApp | IN runApp | result | check |
|
||||
# |-------------|-----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------|
|
||||
# | no | no | use defaults | exhaust all possibilities: if it's missing (runApp does not specify); THEN if it's not in shinyApp appParts$options; THEN use defaults |
|
||||
# | yes | no | use shinyApp | if it's missing (runApp does not specify); THEN if it's in shinyApp appParts$options; THEN use shinyApp |
|
||||
# | no | yes | use runApp | if it's not missing (runApp specifies), use those |
|
||||
# | yes | yes | use runApp | if it's not missing (runApp specifies), use those |
|
||||
#
|
||||
# I tried to make this as compact and intuitive as possible,
|
||||
# given that there are four distinct possibilities to check
|
||||
appOps <- appParts$options
|
||||
findVal <- function(arg, default) {
|
||||
if (arg %in% names(appOps)) appOps[[arg]] else default
|
||||
}
|
||||
|
||||
if (missing(port))
|
||||
port <- findVal("port", port)
|
||||
if (missing(launch.browser))
|
||||
launch.browser <- findVal("launch.browser", launch.browser)
|
||||
if (missing(host))
|
||||
host <- findVal("host", host)
|
||||
if (missing(quiet))
|
||||
quiet <- findVal("quiet", quiet)
|
||||
if (missing(display.mode))
|
||||
display.mode <- findVal("display.mode", display.mode)
|
||||
if (missing(test.mode))
|
||||
test.mode <- findVal("test.mode", test.mode)
|
||||
|
||||
if (is.null(host) || is.na(host)) host <- '0.0.0.0'
|
||||
|
||||
# ============================================================================
|
||||
# Hosted environment
|
||||
# ============================================================================
|
||||
workerId(workerId)
|
||||
|
||||
if (inShinyServer()) {
|
||||
# If SHINY_PORT is set, we're running under Shiny Server. Check the version
|
||||
# to make sure it is compatible. Older versions of Shiny Server don't set
|
||||
# SHINY_SERVER_VERSION, those will return "" which is considered less than
|
||||
# any valid version.
|
||||
ver <- Sys.getenv('SHINY_SERVER_VERSION')
|
||||
if (utils::compareVersion(ver, .shinyServerMinVersion) < 0) {
|
||||
warning('Shiny Server v', .shinyServerMinVersion,
|
||||
' or later is required; please upgrade!')
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Shinytest
|
||||
# ============================================================================
|
||||
# Set the testmode shinyoption so that this can be read by both the
|
||||
# ShinySession and the UI code (which executes separately from the
|
||||
# ShinySession code).
|
||||
shinyOptions(testmode = test.mode)
|
||||
if (test.mode) {
|
||||
message("Running application in test mode.")
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Showcase mode
|
||||
# ============================================================================
|
||||
# Showcase mode is disabled by default; it must be explicitly enabled in
|
||||
# either the DESCRIPTION file for directory-based apps, or via
|
||||
# the display.mode parameter. The latter takes precedence.
|
||||
setShowcaseDefault(0)
|
||||
|
||||
# If appDir specifies a path, and display mode is specified in the
|
||||
# DESCRIPTION file at that path, apply it here.
|
||||
if (is.character(appDir)) {
|
||||
# if appDir specifies a .R file (single-file Shiny app), look for the
|
||||
# DESCRIPTION in the parent directory
|
||||
desc <- file.path.ci(
|
||||
if (tolower(tools::file_ext(appDir)) == "r")
|
||||
dirname(appDir)
|
||||
else
|
||||
appDir, "DESCRIPTION")
|
||||
if (file.exists(desc)) {
|
||||
con <- file(desc, encoding = checkEncoding(desc))
|
||||
on.exit(close(con), add = TRUE)
|
||||
settings <- read.dcf(con)
|
||||
if ("DisplayMode" %in% colnames(settings)) {
|
||||
mode <- settings[1, "DisplayMode"]
|
||||
if (mode == "Showcase") {
|
||||
setShowcaseDefault(1)
|
||||
if ("IncludeWWW" %in% colnames(settings)) {
|
||||
.globals$IncludeWWW <- as.logical(settings[1, "IncludeWWW"])
|
||||
if (is.na(.globals$IncludeWWW)) {
|
||||
stop("In your Description file, `IncludeWWW` ",
|
||||
"must be set to `True` (default) or `False`")
|
||||
}
|
||||
} else {
|
||||
.globals$IncludeWWW <- TRUE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## default is to show the .js, .css and .html files in the www directory
|
||||
## (if not in showcase mode, this variable will simply be ignored)
|
||||
if (is.null(.globals$IncludeWWW) || is.na(.globals$IncludeWWW)) {
|
||||
.globals$IncludeWWW <- TRUE
|
||||
}
|
||||
|
||||
# If display mode is specified as an argument, apply it (overriding the
|
||||
# value specified in DESCRIPTION, if any).
|
||||
display.mode <- match.arg(display.mode)
|
||||
if (display.mode == "normal") {
|
||||
setShowcaseDefault(0)
|
||||
}
|
||||
else if (display.mode == "showcase") {
|
||||
setShowcaseDefault(1)
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Server port
|
||||
# ============================================================================
|
||||
# determine port if we need to
|
||||
if (is.null(port)) {
|
||||
|
||||
# Try up to 20 random ports. If we don't succeed just plow ahead
|
||||
# with the final value we tried, and let the "real" startServer
|
||||
# somewhere down the line fail and throw the error to the user.
|
||||
#
|
||||
# If we (think we) succeed, save the value as .globals$lastPort,
|
||||
# and try that first next time the user wants a random port.
|
||||
|
||||
for (i in 1:20) {
|
||||
if (!is.null(.globals$lastPort)) {
|
||||
port <- .globals$lastPort
|
||||
.globals$lastPort <- NULL
|
||||
}
|
||||
else {
|
||||
# Try up to 20 random ports
|
||||
while (TRUE) {
|
||||
port <- p_randomInt(3000, 8000)
|
||||
# Reject ports in this range that are considered unsafe by Chrome
|
||||
# http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
|
||||
# https://github.com/rstudio/shiny/issues/1784
|
||||
if (!port %in% c(3659, 4045, 6000, 6665:6669, 6697)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Test port to see if we can use it
|
||||
tmp <- try(startServer(host, port, list()), silent=TRUE)
|
||||
if (!inherits(tmp, 'try-error')) {
|
||||
stopServer(tmp)
|
||||
.globals$lastPort <- port
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# onStart/onStop callbacks
|
||||
# ============================================================================
|
||||
# Set up the onStop before we call onStart, so that it gets called even if an
|
||||
# error happens in onStart.
|
||||
if (!is.null(appParts$onStop))
|
||||
on.exit(appParts$onStop(), add = TRUE)
|
||||
if (!is.null(appParts$onStart))
|
||||
appParts$onStart()
|
||||
|
||||
# ============================================================================
|
||||
# Start/stop httpuv app
|
||||
# ============================================================================
|
||||
server <- startApp(appParts, port, host, quiet)
|
||||
|
||||
# Make the httpuv server object accessible. Needed for calling
|
||||
# addResourcePath while app is running.
|
||||
shinyOptions(server = server)
|
||||
|
||||
on.exit({
|
||||
stopServer(server)
|
||||
}, add = TRUE)
|
||||
|
||||
# ============================================================================
|
||||
# Launch web browser
|
||||
# ============================================================================
|
||||
if (!is.character(port)) {
|
||||
browseHost <- host
|
||||
if (identical(host, "0.0.0.0")) {
|
||||
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
|
||||
browseHost <- "127.0.0.1"
|
||||
} else if (identical(host, "::")) {
|
||||
browseHost <- "::1"
|
||||
}
|
||||
|
||||
if (httpuv::ipFamily(browseHost) == 6L) {
|
||||
browseHost <- paste0("[", browseHost, "]")
|
||||
}
|
||||
|
||||
appUrl <- paste("http://", browseHost, ":", port, sep="")
|
||||
if (is.function(launch.browser))
|
||||
launch.browser(appUrl)
|
||||
else if (launch.browser)
|
||||
utils::browseURL(appUrl)
|
||||
} else {
|
||||
appUrl <- NULL
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Application hooks
|
||||
# ============================================================================
|
||||
callAppHook("onAppStart", appUrl)
|
||||
on.exit({
|
||||
callAppHook("onAppStop", appUrl)
|
||||
}, add = TRUE)
|
||||
|
||||
# ============================================================================
|
||||
# Run event loop via httpuv
|
||||
# ============================================================================
|
||||
.globals$reterror <- NULL
|
||||
.globals$retval <- NULL
|
||||
.globals$stopped <- FALSE
|
||||
# Top-level ..stacktraceoff..; matches with ..stacktraceon in observe(),
|
||||
# reactive(), Callbacks$invoke(), and others
|
||||
..stacktraceoff..(
|
||||
captureStackTraces({
|
||||
while (!.globals$stopped) {
|
||||
..stacktracefloor..(serviceApp())
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (isTRUE(.globals$reterror)) {
|
||||
stop(.globals$retval)
|
||||
}
|
||||
else if (.globals$retval$visible)
|
||||
.globals$retval$value
|
||||
else
|
||||
invisible(.globals$retval$value)
|
||||
}
|
||||
|
||||
#' Stop the currently running Shiny app
|
||||
#'
|
||||
#' Stops the currently running Shiny app, returning control to the caller of
|
||||
#' [runApp()].
|
||||
#'
|
||||
#' @param returnValue The value that should be returned from
|
||||
#' [runApp()].
|
||||
#' @export
|
||||
stopApp <- function(returnValue = invisible()) {
|
||||
# reterror will indicate whether retval is an error (i.e. it should be passed
|
||||
# to stop() when the serviceApp loop stops) or a regular value (in which case
|
||||
# it should simply be returned with the appropriate visibility).
|
||||
.globals$reterror <- FALSE
|
||||
..stacktraceoff..(
|
||||
tryCatch(
|
||||
{
|
||||
captureStackTraces(
|
||||
.globals$retval <- withVisible(..stacktraceon..(force(returnValue)))
|
||||
)
|
||||
},
|
||||
error = function(e) {
|
||||
.globals$retval <- e
|
||||
.globals$reterror <- TRUE
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
.globals$stopped <- TRUE
|
||||
httpuv::interrupt()
|
||||
}
|
||||
|
||||
#' Run Shiny Example Applications
|
||||
#'
|
||||
#' Launch Shiny example applications, and optionally, your system's web browser.
|
||||
#'
|
||||
#' @param example The name of the example to run, or `NA` (the default) to
|
||||
#' list the available examples.
|
||||
#' @param launch.browser If true, the system's default web browser will be
|
||||
#' launched automatically after the app is started. Defaults to true in
|
||||
#' interactive sessions only.
|
||||
#' @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.
|
||||
#' @param display.mode The mode in which to display the example. Defaults to
|
||||
#' `showcase`, but may be set to `normal` to see the example without
|
||||
#' code or commentary.
|
||||
#' @inheritParams runApp
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' # List all available examples
|
||||
#' runExample()
|
||||
#'
|
||||
#' # Run one of the examples
|
||||
#' runExample("01_hello")
|
||||
#'
|
||||
#' # Print the directory containing the code for all examples
|
||||
#' system.file("examples", package="shiny")
|
||||
#' }
|
||||
#' @export
|
||||
runExample <- function(example=NA,
|
||||
port=getOption("shiny.port"),
|
||||
launch.browser = getOption('shiny.launch.browser', interactive()),
|
||||
host=getOption('shiny.host', '127.0.0.1'),
|
||||
display.mode=c("auto", "normal", "showcase")) {
|
||||
examplesDir <- system.file('examples', package='shiny')
|
||||
dir <- resolve(examplesDir, example)
|
||||
if (is.null(dir)) {
|
||||
if (is.na(example)) {
|
||||
errFun <- message
|
||||
errMsg <- ''
|
||||
}
|
||||
else {
|
||||
errFun <- stop
|
||||
errMsg <- paste('Example', example, 'does not exist. ')
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#' Run a gadget
|
||||
#'
|
||||
#' Similar to `runApp`, but handles `input$cancel` automatically, and
|
||||
#' if running in RStudio, defaults to viewing the app in the Viewer pane.
|
||||
#'
|
||||
#' @param app Either a Shiny app object as created by
|
||||
#' [`shinyApp()`][shiny] et al, or, a UI object.
|
||||
#' @param server Ignored if `app` is a Shiny app object; otherwise, passed
|
||||
#' along to `shinyApp` (i.e. `shinyApp(ui = app, server = server)`).
|
||||
#' @param port See [`runApp()`][shiny].
|
||||
#' @param viewer Specify where the gadget should be displayed--viewer pane,
|
||||
#' dialog window, or external browser--by passing in a call to one of the
|
||||
#' [viewer()] functions.
|
||||
#' @param stopOnCancel If `TRUE` (the default), then an `observeEvent`
|
||||
#' is automatically created that handles `input$cancel` by calling
|
||||
#' `stopApp()` with an error. Pass `FALSE` if you want to handle
|
||||
#' `input$cancel` yourself.
|
||||
#' @return The value returned by the gadget.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' library(shiny)
|
||||
#'
|
||||
#' ui <- fillPage(...)
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' ...
|
||||
#' }
|
||||
#'
|
||||
#' # Either pass ui/server as separate arguments...
|
||||
#' runGadget(ui, server)
|
||||
#'
|
||||
#' # ...or as a single app object
|
||||
#' runGadget(shinyApp(ui, server))
|
||||
#' }
|
||||
#' @export
|
||||
runGadget <- function(app, server = NULL, port = getOption("shiny.port"),
|
||||
viewer = paneViewer(), stopOnCancel = TRUE) {
|
||||
|
||||
if (!is.shiny.appobj(app)) {
|
||||
app <- shinyApp(app, server)
|
||||
}
|
||||
|
||||
if (isTRUE(stopOnCancel)) {
|
||||
app <- decorateServerFunc(app, function(input, output, session) {
|
||||
observeEvent(input$cancel, {
|
||||
stopApp(stop("User cancel", call. = FALSE))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (is.null(viewer)) {
|
||||
viewer <- utils::browseURL
|
||||
}
|
||||
|
||||
shiny::runApp(app, port = port, launch.browser = viewer)
|
||||
}
|
||||
|
||||
# Add custom functionality to a Shiny app object's server func
|
||||
decorateServerFunc <- function(appobj, serverFunc) {
|
||||
origServerFuncSource <- appobj$serverFuncSource
|
||||
appobj$serverFuncSource <- function() {
|
||||
origServerFunc <- origServerFuncSource()
|
||||
function(input, output, session) {
|
||||
serverFunc(input, output, session)
|
||||
|
||||
# The clientData and session arguments are optional; check if
|
||||
# each exists
|
||||
args <- argsForServerFunc(origServerFunc, session)
|
||||
do.call(origServerFunc, args)
|
||||
}
|
||||
}
|
||||
appobj
|
||||
}
|
||||
169
R/server-resource-paths.R
Normal file
169
R/server-resource-paths.R
Normal file
@@ -0,0 +1,169 @@
|
||||
.globals$resourcePaths <- list()
|
||||
.globals$resources <- list()
|
||||
|
||||
#' Resource Publishing
|
||||
#'
|
||||
#' Add, remove, or list directory of static resources to Shiny's web server,
|
||||
#' with the given path prefix. Primarily intended for package authors to make
|
||||
#' supporting JavaScript/CSS files available to their components.
|
||||
#'
|
||||
#' Shiny provides two ways of serving static files (i.e., resources):
|
||||
#'
|
||||
#' 1. Static files under the `www/` directory are automatically made available
|
||||
#' under a request path that begins with `/`.
|
||||
#' 2. `addResourcePath()` makes static files in a `directoryPath` available
|
||||
#' under a request path that begins with `prefix`.
|
||||
#'
|
||||
#' The second approach is primarily intended for package authors to make
|
||||
#' supporting JavaScript/CSS files available to their components.
|
||||
#'
|
||||
#' Tools for managing static resources published by Shiny's web server:
|
||||
#' * `addResourcePath()` adds a directory of static resources.
|
||||
#' * `resourcePaths()` lists the currently active resource mappings.
|
||||
#' * `removeResourcePath()` removes a directory of static resources.
|
||||
#'
|
||||
#' @param prefix The URL prefix (without slashes). Valid characters are a-z,
|
||||
#' A-Z, 0-9, hyphen, period, and underscore. For example, a value of 'foo'
|
||||
#' means that any request paths that begin with '/foo' will be mapped to the
|
||||
#' given directory.
|
||||
#' @param directoryPath The directory that contains the static resources to be
|
||||
#' served.
|
||||
#'
|
||||
#' @rdname resourcePaths
|
||||
#' @seealso [singleton()]
|
||||
#'
|
||||
#' @examples
|
||||
#' addResourcePath('datasets', system.file('data', package='datasets'))
|
||||
#' resourcePaths()
|
||||
#' removeResourcePath('datasets')
|
||||
#' resourcePaths()
|
||||
#'
|
||||
#' # make sure all resources are removed
|
||||
#' lapply(names(resourcePaths()), removeResourcePath)
|
||||
#' @export
|
||||
addResourcePath <- function(prefix, directoryPath) {
|
||||
if (length(prefix) != 1) stop("prefix must be of length 1")
|
||||
if (grepl("^\\.+$", prefix)) stop("prefix can't be composed of dots only")
|
||||
if (!grepl('[a-z0-9\\-_.]+$', prefix, ignore.case = TRUE, perl = TRUE)) {
|
||||
stop("addResourcePath called with invalid prefix; please see documentation")
|
||||
}
|
||||
if (prefix %in% c('shared')) {
|
||||
stop("addResourcePath called with the reserved prefix '", prefix, "'; ",
|
||||
"please use a different prefix")
|
||||
}
|
||||
normalizedPath <- tryCatch(normalizePath(directoryPath, mustWork = TRUE),
|
||||
error = function(e) {
|
||||
stop("Couldn't normalize path in `addResourcePath`, with arguments: ",
|
||||
"`prefix` = '", prefix, "'; `directoryPath` = '" , directoryPath, "'")
|
||||
}
|
||||
)
|
||||
|
||||
# # Often times overwriting a resource path is "what you want",
|
||||
# # but sometimes it can lead to difficult to diagnose issues
|
||||
# # (e.g. an implict dependency might set a resource path that
|
||||
# # conflicts with what you, the app author, are trying to register)
|
||||
# # Note that previous versions of shiny used to warn about this case,
|
||||
# # but it was eventually removed since it caused confusion (#567).
|
||||
# # It seems a good compromise is to throw a more information message.
|
||||
# if (getOption("shiny.resourcePathChanges", FALSE) &&
|
||||
# prefix %in% names(.globals$resourcePaths)) {
|
||||
# existingPath <- .globals$resourcePaths[[prefix]]$path
|
||||
# if (normalizedPath != existingPath) {
|
||||
# message(
|
||||
# "The resource path '", prefix, "' used to point to ",
|
||||
# existingPath, ", but it now points to ", normalizedPath, ". ",
|
||||
# "If your app doesn't work as expected, you may want to ",
|
||||
# "choose a different prefix name."
|
||||
# )
|
||||
# }
|
||||
# }
|
||||
|
||||
# If a shiny app is currently running, dynamically register this path with
|
||||
# the corresponding httpuv server object.
|
||||
if (!is.null(getShinyOption("server", default = NULL)))
|
||||
{
|
||||
getShinyOption("server")$setStaticPath(.list = stats::setNames(normalizedPath, prefix))
|
||||
}
|
||||
|
||||
# .globals$resourcePaths and .globals$resources persist across runs of applications.
|
||||
.globals$resourcePaths[[prefix]] <- staticPath(normalizedPath)
|
||||
# This is necessary because resourcePaths is only for serving assets out of C++;
|
||||
# to support subapps, we also need assets to be served out of R, because those
|
||||
# URLs are rewritten by R code (i.e. routeHandler) before they can be matched to
|
||||
# a resource path.
|
||||
.globals$resources[[prefix]] <- list(
|
||||
directoryPath = normalizedPath,
|
||||
func = staticHandler(normalizedPath)
|
||||
)
|
||||
}
|
||||
|
||||
#' @rdname resourcePaths
|
||||
#' @export
|
||||
resourcePaths <- function() {
|
||||
urls <- names(.globals$resourcePaths)
|
||||
paths <- vapply(.globals$resourcePaths, function(x) x$path, character(1))
|
||||
stats::setNames(paths, urls)
|
||||
}
|
||||
|
||||
hasResourcePath <- function(prefix) {
|
||||
prefix %in% names(resourcePaths())
|
||||
}
|
||||
|
||||
#' @rdname resourcePaths
|
||||
#' @export
|
||||
removeResourcePath <- function(prefix) {
|
||||
if (length(prefix) > 1) stop("`prefix` must be of length 1.")
|
||||
if (!hasResourcePath(prefix)) {
|
||||
warning("Resource ", prefix, " not found.")
|
||||
return(invisible(FALSE))
|
||||
}
|
||||
.globals$resourcePaths[[prefix]] <- NULL
|
||||
.globals$resources[[prefix]] <- NULL
|
||||
invisible(TRUE)
|
||||
}
|
||||
|
||||
# This function handles any GET request with two or more path elements where the
|
||||
# first path element matches a prefix that was previously added using
|
||||
# addResourcePath().
|
||||
#
|
||||
# For example, if `addResourcePath("foo", "~/bar")` was called, then a GET
|
||||
# request for /foo/one/two.html would rewrite the PATH_INFO as /one/two.html and
|
||||
# send it to the resource path function for "foo". As of this writing, that
|
||||
# function will always be a staticHandler, which serves up a file if it exists
|
||||
# and NULL if it does not.
|
||||
#
|
||||
# Since Shiny 1.3.x, assets registered via addResourcePath should mostly be
|
||||
# served out of httpuv's native static file serving features. However, in the
|
||||
# specific case of subapps, the R code path must be used, because subapps insert
|
||||
# a giant random ID into the beginning of the URL that must be stripped off by
|
||||
# an R route handler (see addSubApp()).
|
||||
resourcePathHandler <- function(req) {
|
||||
if (!identical(req$REQUEST_METHOD, 'GET'))
|
||||
return(NULL)
|
||||
|
||||
# e.g. "/foo/one/two.html"
|
||||
path <- req$PATH_INFO
|
||||
|
||||
match <- regexpr('^/([^/]+)/', path, perl=TRUE)
|
||||
if (match == -1)
|
||||
return(NULL)
|
||||
len <- attr(match, 'capture.length')
|
||||
# e.g. "foo"
|
||||
prefix <- substr(path, 2, 2 + len - 1)
|
||||
|
||||
resInfo <- .globals$resources[[prefix]]
|
||||
if (is.null(resInfo))
|
||||
return(NULL)
|
||||
|
||||
# e.g. "/one/two.html"
|
||||
suffix <- substr(path, 2 + len, nchar(path))
|
||||
|
||||
# Create a new request that's a clone of the current request, but adjust
|
||||
# PATH_INFO and SCRIPT_NAME to reflect that we have already matched the first
|
||||
# path element (e.g. "/foo"). See routeHandler() for more info.
|
||||
subreq <- as.environment(as.list(req, all.names=TRUE))
|
||||
subreq$PATH_INFO <- suffix
|
||||
subreq$SCRIPT_NAME <- paste(subreq$SCRIPT_NAME, substr(path, 1, 2 + len), sep='')
|
||||
|
||||
return(resInfo$func(subreq))
|
||||
}
|
||||
774
R/server.R
774
R/server.R
@@ -22,185 +22,16 @@ registerClient <- function(client) {
|
||||
}
|
||||
|
||||
|
||||
.globals$resourcePaths <- list()
|
||||
.globals$resources <- list()
|
||||
|
||||
.globals$showcaseDefault <- 0
|
||||
|
||||
.globals$showcaseOverride <- FALSE
|
||||
|
||||
#' Resource Publishing
|
||||
#'
|
||||
#' Add, remove, or list directory of static resources to Shiny's web server,
|
||||
#' with the given path prefix. Primarily intended for package authors to make
|
||||
#' supporting JavaScript/CSS files available to their components.
|
||||
#'
|
||||
#' Shiny provides two ways of serving static files (i.e., resources):
|
||||
#'
|
||||
#' 1. Static files under the `www/` directory are automatically made available
|
||||
#' under a request path that begins with `/`.
|
||||
#' 2. `addResourcePath()` makes static files in a `directoryPath` available
|
||||
#' under a request path that begins with `prefix`.
|
||||
#'
|
||||
#' The second approach is primarily intended for package authors to make
|
||||
#' supporting JavaScript/CSS files available to their components.
|
||||
#'
|
||||
#' Tools for managing static resources published by Shiny's web server:
|
||||
#' * `addResourcePath()` adds a directory of static resources.
|
||||
#' * `resourcePaths()` lists the currently active resource mappings.
|
||||
#' * `removeResourcePath()` removes a directory of static resources.
|
||||
#'
|
||||
#' @param prefix The URL prefix (without slashes). Valid characters are a-z,
|
||||
#' A-Z, 0-9, hyphen, period, and underscore. For example, a value of 'foo'
|
||||
#' means that any request paths that begin with '/foo' will be mapped to the
|
||||
#' given directory.
|
||||
#' @param directoryPath The directory that contains the static resources to be
|
||||
#' served.
|
||||
#'
|
||||
#' @rdname resourcePaths
|
||||
#' @seealso [singleton()]
|
||||
#'
|
||||
#' @examples
|
||||
#' addResourcePath('datasets', system.file('data', package='datasets'))
|
||||
#' resourcePaths()
|
||||
#' removeResourcePath('datasets')
|
||||
#' resourcePaths()
|
||||
#'
|
||||
#' # make sure all resources are removed
|
||||
#' lapply(names(resourcePaths()), removeResourcePath)
|
||||
#' @export
|
||||
addResourcePath <- function(prefix, directoryPath) {
|
||||
if (length(prefix) != 1) stop("prefix must be of length 1")
|
||||
if (grepl("^\\.+$", prefix)) stop("prefix can't be composed of dots only")
|
||||
if (!grepl('[a-z0-9\\-_.]+$', prefix, ignore.case = TRUE, perl = TRUE)) {
|
||||
stop("addResourcePath called with invalid prefix; please see documentation")
|
||||
}
|
||||
if (prefix %in% c('shared')) {
|
||||
stop("addResourcePath called with the reserved prefix '", prefix, "'; ",
|
||||
"please use a different prefix")
|
||||
}
|
||||
normalizedPath <- tryCatch(normalizePath(directoryPath, mustWork = TRUE),
|
||||
error = function(e) {
|
||||
stop("Couldn't normalize path in `addResourcePath`, with arguments: ",
|
||||
"`prefix` = '", prefix, "'; `directoryPath` = '" , directoryPath, "'")
|
||||
}
|
||||
)
|
||||
|
||||
# # Often times overwriting a resource path is "what you want",
|
||||
# # but sometimes it can lead to difficult to diagnose issues
|
||||
# # (e.g. an implict dependency might set a resource path that
|
||||
# # conflicts with what you, the app author, are trying to register)
|
||||
# # Note that previous versions of shiny used to warn about this case,
|
||||
# # but it was eventually removed since it caused confusion (#567).
|
||||
# # It seems a good compromise is to throw a more information message.
|
||||
# if (getOption("shiny.resourcePathChanges", FALSE) &&
|
||||
# prefix %in% names(.globals$resourcePaths)) {
|
||||
# existingPath <- .globals$resourcePaths[[prefix]]$path
|
||||
# if (normalizedPath != existingPath) {
|
||||
# message(
|
||||
# "The resource path '", prefix, "' used to point to ",
|
||||
# existingPath, ", but it now points to ", normalizedPath, ". ",
|
||||
# "If your app doesn't work as expected, you may want to ",
|
||||
# "choose a different prefix name."
|
||||
# )
|
||||
# }
|
||||
# }
|
||||
|
||||
# If a shiny app is currently running, dynamically register this path with
|
||||
# the corresponding httpuv server object.
|
||||
if (!is.null(getShinyOption("server")))
|
||||
{
|
||||
getShinyOption("server")$setStaticPath(.list = stats::setNames(normalizedPath, prefix))
|
||||
}
|
||||
|
||||
# .globals$resourcePaths and .globals$resources persist across runs of applications.
|
||||
.globals$resourcePaths[[prefix]] <- staticPath(normalizedPath)
|
||||
# This is necessary because resourcePaths is only for serving assets out of C++;
|
||||
# to support subapps, we also need assets to be served out of R, because those
|
||||
# URLs are rewritten by R code (i.e. routeHandler) before they can be matched to
|
||||
# a resource path.
|
||||
.globals$resources[[prefix]] <- list(
|
||||
directoryPath = normalizedPath,
|
||||
func = staticHandler(normalizedPath)
|
||||
)
|
||||
}
|
||||
|
||||
#' @rdname resourcePaths
|
||||
#' @export
|
||||
resourcePaths <- function() {
|
||||
urls <- names(.globals$resourcePaths)
|
||||
paths <- vapply(.globals$resourcePaths, function(x) x$path, character(1))
|
||||
stats::setNames(paths, urls)
|
||||
}
|
||||
|
||||
hasResourcePath <- function(prefix) {
|
||||
prefix %in% names(resourcePaths())
|
||||
}
|
||||
|
||||
#' @rdname resourcePaths
|
||||
#' @export
|
||||
removeResourcePath <- function(prefix) {
|
||||
if (length(prefix) > 1) stop("`prefix` must be of length 1.")
|
||||
if (!hasResourcePath(prefix)) {
|
||||
warning("Resource ", prefix, " not found.")
|
||||
return(invisible(FALSE))
|
||||
}
|
||||
.globals$resourcePaths[[prefix]] <- NULL
|
||||
.globals$resources[[prefix]] <- NULL
|
||||
invisible(TRUE)
|
||||
}
|
||||
|
||||
|
||||
|
||||
# This function handles any GET request with two or more path elements where the
|
||||
# first path element matches a prefix that was previously added using
|
||||
# addResourcePath().
|
||||
#
|
||||
# For example, if `addResourcePath("foo", "~/bar")` was called, then a GET
|
||||
# request for /foo/one/two.html would rewrite the PATH_INFO as /one/two.html and
|
||||
# send it to the resource path function for "foo". As of this writing, that
|
||||
# function will always be a staticHandler, which serves up a file if it exists
|
||||
# and NULL if it does not.
|
||||
#
|
||||
# Since Shiny 1.3.x, assets registered via addResourcePath should mostly be
|
||||
# served out of httpuv's native static file serving features. However, in the
|
||||
# specific case of subapps, the R code path must be used, because subapps insert
|
||||
# a giant random ID into the beginning of the URL that must be stripped off by
|
||||
# an R route handler (see addSubApp()).
|
||||
resourcePathHandler <- function(req) {
|
||||
if (!identical(req$REQUEST_METHOD, 'GET'))
|
||||
return(NULL)
|
||||
|
||||
# e.g. "/foo/one/two.html"
|
||||
path <- req$PATH_INFO
|
||||
|
||||
match <- regexpr('^/([^/]+)/', path, perl=TRUE)
|
||||
if (match == -1)
|
||||
return(NULL)
|
||||
len <- attr(match, 'capture.length')
|
||||
# e.g. "foo"
|
||||
prefix <- substr(path, 2, 2 + len - 1)
|
||||
|
||||
resInfo <- .globals$resources[[prefix]]
|
||||
if (is.null(resInfo))
|
||||
return(NULL)
|
||||
|
||||
# e.g. "/one/two.html"
|
||||
suffix <- substr(path, 2 + len, nchar(path))
|
||||
|
||||
# Create a new request that's a clone of the current request, but adjust
|
||||
# PATH_INFO and SCRIPT_NAME to reflect that we have already matched the first
|
||||
# path element (e.g. "/foo"). See routeHandler() for more info.
|
||||
subreq <- as.environment(as.list(req, all.names=TRUE))
|
||||
subreq$PATH_INFO <- suffix
|
||||
subreq$SCRIPT_NAME <- paste(subreq$SCRIPT_NAME, substr(path, 1, 2 + len), sep='')
|
||||
|
||||
return(resInfo$func(subreq))
|
||||
}
|
||||
|
||||
#' Define Server Functionality
|
||||
#'
|
||||
#' Defines the server-side logic of the Shiny application. This generally
|
||||
#' @description \lifecycle{superseded}
|
||||
#'
|
||||
#' @description Defines the server-side logic of the Shiny application. This generally
|
||||
#' involves creating functions that map user inputs to various kinds of output.
|
||||
#' In older versions of Shiny, it was necessary to call `shinyServer()` in
|
||||
#' the `server.R` file, but this is no longer required as of Shiny 0.10.
|
||||
@@ -218,7 +49,7 @@ resourcePathHandler <- function(req) {
|
||||
#' optional `session` parameter, which is used when greater control is
|
||||
#' needed.
|
||||
#'
|
||||
#' See the [tutorial](http://rstudio.github.com/shiny/tutorial/) for more
|
||||
#' See the [tutorial](https://rstudio.github.io/shiny/tutorial/) for more
|
||||
#' on how to write a server function.
|
||||
#'
|
||||
#' @param func The server function for this application. See the details section
|
||||
@@ -247,6 +78,17 @@ resourcePathHandler <- function(req) {
|
||||
#' @export
|
||||
#' @keywords internal
|
||||
shinyServer <- function(func) {
|
||||
if (in_devmode()) {
|
||||
shinyDeprecated(
|
||||
"0.10.0", "shinyServer()",
|
||||
details = paste0(
|
||||
"When removing `shinyServer()`, ",
|
||||
"ensure that the last expression returned from server.R ",
|
||||
"is the function normally supplied to `shinyServer(func)`."
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
.globals$server <- list(func)
|
||||
invisible(func)
|
||||
}
|
||||
@@ -308,7 +150,7 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
|
||||
}
|
||||
|
||||
if (identical(ws$request$PATH_INFO, "/autoreload/")) {
|
||||
if (!getOption("shiny.autoreload", FALSE)) {
|
||||
if (!get_devmode_option("shiny.autoreload", FALSE)) {
|
||||
ws$close()
|
||||
return(TRUE)
|
||||
}
|
||||
@@ -645,9 +487,6 @@ serviceApp <- function() {
|
||||
|
||||
.shinyServerMinVersion <- '0.3.4'
|
||||
|
||||
# Global flag that's TRUE whenever we're inside of the scope of a call to runApp
|
||||
.globals$running <- FALSE
|
||||
|
||||
#' Check whether a Shiny application is running
|
||||
#'
|
||||
#' This function tests whether a Shiny application is currently running.
|
||||
@@ -656,588 +495,9 @@ serviceApp <- function() {
|
||||
#' `FALSE`.
|
||||
#' @export
|
||||
isRunning <- function() {
|
||||
.globals$running
|
||||
!is.null(getCurrentAppState())
|
||||
}
|
||||
|
||||
#' Run Shiny Application
|
||||
#'
|
||||
#' Runs a Shiny application. This function normally does not return; interrupt R
|
||||
#' to stop the application (usually by pressing Ctrl+C or Esc).
|
||||
#'
|
||||
#' The host parameter was introduced in Shiny 0.9.0. Its default value of
|
||||
#' `"127.0.0.1"` means that, contrary to previous versions of Shiny, only
|
||||
#' the current machine can access locally hosted Shiny apps. To allow other
|
||||
#' clients to connect, use the value `"0.0.0.0"` instead (which was the
|
||||
#' value that was hard-coded into Shiny in 0.8.0 and earlier).
|
||||
#'
|
||||
#' @param appDir The application to run. Should be one of the following:
|
||||
#' \itemize{
|
||||
#' \item A directory containing `server.R`, plus, either `ui.R` or
|
||||
#' a `www` directory that contains the file `index.html`.
|
||||
#' \item A directory containing `app.R`.
|
||||
#' \item An `.R` file containing a Shiny application, ending with an
|
||||
#' expression that produces a Shiny app object.
|
||||
#' \item A list with `ui` and `server` components.
|
||||
#' \item A Shiny app object created by [shinyApp()].
|
||||
#' }
|
||||
#' @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
|
||||
#' `options(shiny.port = XX)`), then that port will be used. Otherwise,
|
||||
#' use a random port.
|
||||
#' @param launch.browser If true, the system's default web browser will be
|
||||
#' launched automatically after the app is started. Defaults to true in
|
||||
#' interactive sessions only. This value of this parameter can also be a
|
||||
#' function to call with the application's URL.
|
||||
#' @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
|
||||
#' Details.
|
||||
#' @param workerId Can generally be ignored. Exists to help some editions of
|
||||
#' Shiny Server Pro route requests to the correct process.
|
||||
#' @param quiet Should Shiny status messages be shown? Defaults to FALSE.
|
||||
#' @param display.mode The mode in which to display the application. If set to
|
||||
#' the value `"showcase"`, shows application code and metadata from a
|
||||
#' `DESCRIPTION` file in the application directory alongside the
|
||||
#' application. If set to `"normal"`, displays the application normally.
|
||||
#' Defaults to `"auto"`, which displays the application in the mode given
|
||||
#' in its `DESCRIPTION` file, if any.
|
||||
#' @param test.mode Should the application be launched in test mode? This is
|
||||
#' only used for recording or running automated tests. Defaults to the
|
||||
#' `shiny.testmode` option, or FALSE if the option is not set.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' # Start app in the current working directory
|
||||
#' runApp()
|
||||
#'
|
||||
#' # Start app in a subdirectory called myapp
|
||||
#' runApp("myapp")
|
||||
#' }
|
||||
#'
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' options(device.ask.default = FALSE)
|
||||
#'
|
||||
#' # Apps can be run without a server.r and ui.r file
|
||||
#' runApp(list(
|
||||
#' ui = bootstrapPage(
|
||||
#' numericInput('n', 'Number of obs', 100),
|
||||
#' plotOutput('plot')
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$plot <- renderPlot({ hist(runif(input$n)) })
|
||||
#' }
|
||||
#' ))
|
||||
#'
|
||||
#'
|
||||
#' # Running a Shiny app object
|
||||
#' app <- shinyApp(
|
||||
#' ui = bootstrapPage(
|
||||
#' numericInput('n', 'Number of obs', 100),
|
||||
#' plotOutput('plot')
|
||||
#' ),
|
||||
#' server = function(input, output) {
|
||||
#' output$plot <- renderPlot({ hist(runif(input$n)) })
|
||||
#' }
|
||||
#' )
|
||||
#' runApp(app)
|
||||
#' }
|
||||
#' @export
|
||||
runApp <- function(appDir=getwd(),
|
||||
port=getOption('shiny.port'),
|
||||
launch.browser=getOption('shiny.launch.browser',
|
||||
interactive()),
|
||||
host=getOption('shiny.host', '127.0.0.1'),
|
||||
workerId="", quiet=FALSE,
|
||||
display.mode=c("auto", "normal", "showcase"),
|
||||
test.mode=getOption('shiny.testmode', FALSE)) {
|
||||
on.exit({
|
||||
handlerManager$clear()
|
||||
}, add = TRUE)
|
||||
|
||||
if (.globals$running) {
|
||||
stop("Can't call `runApp()` from within `runApp()`. If your ",
|
||||
"application code contains `runApp()`, please remove it.")
|
||||
}
|
||||
.globals$running <- TRUE
|
||||
on.exit({
|
||||
.globals$running <- FALSE
|
||||
}, add = TRUE)
|
||||
|
||||
# Enable per-app Shiny options, for shinyOptions() and getShinyOption().
|
||||
oldOptionSet <- .globals$options
|
||||
on.exit({
|
||||
.globals$options <- oldOptionSet
|
||||
},add = TRUE)
|
||||
|
||||
# A unique identifier associated with this run of this application. It is
|
||||
# shared across sessions.
|
||||
shinyOptions(appToken = createUniqueId(8))
|
||||
|
||||
# Make warnings print immediately
|
||||
# Set pool.scheduler to support pool package
|
||||
ops <- options(
|
||||
# Raise warn level to 1, but don't lower it
|
||||
warn = max(1, getOption("warn", default = 1)),
|
||||
pool.scheduler = scheduleTask
|
||||
)
|
||||
on.exit(options(ops), add = TRUE)
|
||||
|
||||
# Set up default cache for app.
|
||||
if (is.null(getShinyOption("cache"))) {
|
||||
shinyOptions(cache = MemoryCache$new())
|
||||
}
|
||||
|
||||
# Invoke user-defined onStop callbacks, before the application's internal
|
||||
# onStop callbacks.
|
||||
on.exit({
|
||||
.globals$onStopCallbacks$invoke()
|
||||
.globals$onStopCallbacks <- Callbacks$new()
|
||||
}, add = TRUE)
|
||||
|
||||
require(shiny)
|
||||
|
||||
appParts <- as.shiny.appobj(appDir)
|
||||
|
||||
# The lines below set some of the app's running options, which
|
||||
# can be:
|
||||
# - left unspeficied (in which case the arguments' default
|
||||
# values from `runApp` kick in);
|
||||
# - passed through `shinyApp`
|
||||
# - passed through `runApp` (this function)
|
||||
# - passed through both `shinyApp` and `runApp` (the latter
|
||||
# takes precedence)
|
||||
#
|
||||
# Matrix of possibilities:
|
||||
# | IN shinyApp | IN runApp | result | check |
|
||||
# |-------------|-----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------|
|
||||
# | no | no | use defaults | exhaust all possibilities: if it's missing (runApp does not specify); THEN if it's not in shinyApp appParts$options; THEN use defaults |
|
||||
# | yes | no | use shinyApp | if it's missing (runApp does not specify); THEN if it's in shinyApp appParts$options; THEN use shinyApp |
|
||||
# | no | yes | use runApp | if it's not missing (runApp specifies), use those |
|
||||
# | yes | yes | use runApp | if it's not missing (runApp specifies), use those |
|
||||
#
|
||||
# I tried to make this as compact and intuitive as possible,
|
||||
# given that there are four distinct possibilities to check
|
||||
appOps <- appParts$options
|
||||
findVal <- function(arg, default) {
|
||||
if (arg %in% names(appOps)) appOps[[arg]] else default
|
||||
}
|
||||
|
||||
if (missing(port))
|
||||
port <- findVal("port", port)
|
||||
if (missing(launch.browser))
|
||||
launch.browser <- findVal("launch.browser", launch.browser)
|
||||
if (missing(host))
|
||||
host <- findVal("host", host)
|
||||
if (missing(quiet))
|
||||
quiet <- findVal("quiet", quiet)
|
||||
if (missing(display.mode))
|
||||
display.mode <- findVal("display.mode", display.mode)
|
||||
if (missing(test.mode))
|
||||
test.mode <- findVal("test.mode", test.mode)
|
||||
|
||||
if (is.null(host) || is.na(host)) host <- '0.0.0.0'
|
||||
|
||||
workerId(workerId)
|
||||
|
||||
if (inShinyServer()) {
|
||||
# If SHINY_PORT is set, we're running under Shiny Server. Check the version
|
||||
# to make sure it is compatible. Older versions of Shiny Server don't set
|
||||
# SHINY_SERVER_VERSION, those will return "" which is considered less than
|
||||
# any valid version.
|
||||
ver <- Sys.getenv('SHINY_SERVER_VERSION')
|
||||
if (utils::compareVersion(ver, .shinyServerMinVersion) < 0) {
|
||||
warning('Shiny Server v', .shinyServerMinVersion,
|
||||
' or later is required; please upgrade!')
|
||||
}
|
||||
}
|
||||
|
||||
# Showcase mode is disabled by default; it must be explicitly enabled in
|
||||
# either the DESCRIPTION file for directory-based apps, or via
|
||||
# the display.mode parameter. The latter takes precedence.
|
||||
setShowcaseDefault(0)
|
||||
|
||||
.globals$testMode <- test.mode
|
||||
if (test.mode) {
|
||||
message("Running application in test mode.")
|
||||
}
|
||||
|
||||
# If appDir specifies a path, and display mode is specified in the
|
||||
# DESCRIPTION file at that path, apply it here.
|
||||
if (is.character(appDir)) {
|
||||
# if appDir specifies a .R file (single-file Shiny app), look for the
|
||||
# DESCRIPTION in the parent directory
|
||||
desc <- file.path.ci(
|
||||
if (tolower(tools::file_ext(appDir)) == "r")
|
||||
dirname(appDir)
|
||||
else
|
||||
appDir, "DESCRIPTION")
|
||||
if (file.exists(desc)) {
|
||||
con <- file(desc, encoding = checkEncoding(desc))
|
||||
on.exit(close(con), add = TRUE)
|
||||
settings <- read.dcf(con)
|
||||
if ("DisplayMode" %in% colnames(settings)) {
|
||||
mode <- settings[1, "DisplayMode"]
|
||||
if (mode == "Showcase") {
|
||||
setShowcaseDefault(1)
|
||||
if ("IncludeWWW" %in% colnames(settings)) {
|
||||
.globals$IncludeWWW <- as.logical(settings[1, "IncludeWWW"])
|
||||
if (is.na(.globals$IncludeWWW)) {
|
||||
stop("In your Description file, `IncludeWWW` ",
|
||||
"must be set to `True` (default) or `False`")
|
||||
}
|
||||
} else {
|
||||
.globals$IncludeWWW <- TRUE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## default is to show the .js, .css and .html files in the www directory
|
||||
## (if not in showcase mode, this variable will simply be ignored)
|
||||
if (is.null(.globals$IncludeWWW) || is.na(.globals$IncludeWWW)) {
|
||||
.globals$IncludeWWW <- TRUE
|
||||
}
|
||||
|
||||
# If display mode is specified as an argument, apply it (overriding the
|
||||
# value specified in DESCRIPTION, if any).
|
||||
display.mode <- match.arg(display.mode)
|
||||
if (display.mode == "normal") {
|
||||
setShowcaseDefault(0)
|
||||
}
|
||||
else if (display.mode == "showcase") {
|
||||
setShowcaseDefault(1)
|
||||
}
|
||||
|
||||
# determine port if we need to
|
||||
if (is.null(port)) {
|
||||
|
||||
# Try up to 20 random ports. If we don't succeed just plow ahead
|
||||
# with the final value we tried, and let the "real" startServer
|
||||
# somewhere down the line fail and throw the error to the user.
|
||||
#
|
||||
# If we (think we) succeed, save the value as .globals$lastPort,
|
||||
# and try that first next time the user wants a random port.
|
||||
|
||||
for (i in 1:20) {
|
||||
if (!is.null(.globals$lastPort)) {
|
||||
port <- .globals$lastPort
|
||||
.globals$lastPort <- NULL
|
||||
}
|
||||
else {
|
||||
# Try up to 20 random ports
|
||||
while (TRUE) {
|
||||
port <- p_randomInt(3000, 8000)
|
||||
# Reject ports in this range that are considered unsafe by Chrome
|
||||
# http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
|
||||
# https://github.com/rstudio/shiny/issues/1784
|
||||
if (!port %in% c(3659, 4045, 6000, 6665:6669, 6697)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Test port to see if we can use it
|
||||
tmp <- try(startServer(host, port, list()), silent=TRUE)
|
||||
if (!inherits(tmp, 'try-error')) {
|
||||
stopServer(tmp)
|
||||
.globals$lastPort <- port
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Extract appOptions (which is a list) and store them as shinyOptions, for
|
||||
# this app. (This is the only place we have to store settings that are
|
||||
# accessible both the UI and server portion of the app.)
|
||||
unconsumeAppOptions(appParts$appOptions)
|
||||
|
||||
# Set up the onStop before we call onStart, so that it gets called even if an
|
||||
# error happens in onStart.
|
||||
if (!is.null(appParts$onStop))
|
||||
on.exit(appParts$onStop(), add = TRUE)
|
||||
if (!is.null(appParts$onStart))
|
||||
appParts$onStart()
|
||||
|
||||
server <- startApp(appParts, port, host, quiet)
|
||||
|
||||
# Make the httpuv server object accessible. Needed for calling
|
||||
# addResourcePath while app is running.
|
||||
shinyOptions(server = server)
|
||||
|
||||
on.exit({
|
||||
stopServer(server)
|
||||
}, add = TRUE)
|
||||
|
||||
if (!is.character(port)) {
|
||||
browseHost <- host
|
||||
if (identical(host, "0.0.0.0")) {
|
||||
# http://0.0.0.0/ doesn't work on QtWebKit (i.e. RStudio viewer)
|
||||
browseHost <- "127.0.0.1"
|
||||
} else if (identical(host, "::")) {
|
||||
browseHost <- "::1"
|
||||
}
|
||||
|
||||
if (httpuv::ipFamily(browseHost) == 6L) {
|
||||
browseHost <- paste0("[", browseHost, "]")
|
||||
}
|
||||
|
||||
appUrl <- paste("http://", browseHost, ":", port, sep="")
|
||||
if (is.function(launch.browser))
|
||||
launch.browser(appUrl)
|
||||
else if (launch.browser)
|
||||
utils::browseURL(appUrl)
|
||||
} else {
|
||||
appUrl <- NULL
|
||||
}
|
||||
|
||||
# call application hooks
|
||||
callAppHook("onAppStart", appUrl)
|
||||
on.exit({
|
||||
callAppHook("onAppStop", appUrl)
|
||||
}, add = TRUE)
|
||||
|
||||
.globals$reterror <- NULL
|
||||
.globals$retval <- NULL
|
||||
.globals$stopped <- FALSE
|
||||
# Top-level ..stacktraceoff..; matches with ..stacktraceon in observe(),
|
||||
# reactive(), Callbacks$invoke(), and others
|
||||
..stacktraceoff..(
|
||||
captureStackTraces({
|
||||
while (!.globals$stopped) {
|
||||
..stacktracefloor..(serviceApp())
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (isTRUE(.globals$reterror)) {
|
||||
stop(.globals$retval)
|
||||
}
|
||||
else if (.globals$retval$visible)
|
||||
.globals$retval$value
|
||||
else
|
||||
invisible(.globals$retval$value)
|
||||
}
|
||||
|
||||
#' Stop the currently running Shiny app
|
||||
#'
|
||||
#' Stops the currently running Shiny app, returning control to the caller of
|
||||
#' [runApp()].
|
||||
#'
|
||||
#' @param returnValue The value that should be returned from
|
||||
#' [runApp()].
|
||||
#' @export
|
||||
stopApp <- function(returnValue = invisible()) {
|
||||
# reterror will indicate whether retval is an error (i.e. it should be passed
|
||||
# to stop() when the serviceApp loop stops) or a regular value (in which case
|
||||
# it should simply be returned with the appropriate visibility).
|
||||
.globals$reterror <- FALSE
|
||||
..stacktraceoff..(
|
||||
tryCatch(
|
||||
{
|
||||
captureStackTraces(
|
||||
.globals$retval <- withVisible(..stacktraceon..(force(returnValue)))
|
||||
)
|
||||
},
|
||||
error = function(e) {
|
||||
.globals$retval <- e
|
||||
.globals$reterror <- TRUE
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
.globals$stopped <- TRUE
|
||||
httpuv::interrupt()
|
||||
}
|
||||
|
||||
#' Run Shiny Example Applications
|
||||
#'
|
||||
#' Launch Shiny example applications, and optionally, your system's web browser.
|
||||
#'
|
||||
#' @param example The name of the example to run, or `NA` (the default) to
|
||||
#' list the available examples.
|
||||
#' @param launch.browser If true, the system's default web browser will be
|
||||
#' launched automatically after the app is started. Defaults to true in
|
||||
#' interactive sessions only.
|
||||
#' @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.
|
||||
#' @param display.mode The mode in which to display the example. Defaults to
|
||||
#' `showcase`, but may be set to `normal` to see the example without
|
||||
#' code or commentary.
|
||||
#' @inheritParams runApp
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' # List all available examples
|
||||
#' runExample()
|
||||
#'
|
||||
#' # Run one of the examples
|
||||
#' runExample("01_hello")
|
||||
#'
|
||||
#' # Print the directory containing the code for all examples
|
||||
#' system.file("examples", package="shiny")
|
||||
#' }
|
||||
#' @export
|
||||
runExample <- function(example=NA,
|
||||
port=getOption("shiny.port"),
|
||||
launch.browser=getOption('shiny.launch.browser',
|
||||
interactive()),
|
||||
host=getOption('shiny.host', '127.0.0.1'),
|
||||
display.mode=c("auto", "normal", "showcase")) {
|
||||
examplesDir <- system.file('examples', package='shiny')
|
||||
dir <- resolve(examplesDir, example)
|
||||
if (is.null(dir)) {
|
||||
if (is.na(example)) {
|
||||
errFun <- message
|
||||
errMsg <- ''
|
||||
}
|
||||
else {
|
||||
errFun <- stop
|
||||
errMsg <- paste('Example', example, 'does not exist. ')
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#' Run a gadget
|
||||
#'
|
||||
#' Similar to `runApp`, but handles `input$cancel` automatically, and
|
||||
#' if running in RStudio, defaults to viewing the app in the Viewer pane.
|
||||
#'
|
||||
#' @param app Either a Shiny app object as created by
|
||||
#' [`shinyApp()`][shiny] et al, or, a UI object.
|
||||
#' @param server Ignored if `app` is a Shiny app object; otherwise, passed
|
||||
#' along to `shinyApp` (i.e. `shinyApp(ui = app, server = server)`).
|
||||
#' @param port See [`runApp()`][shiny].
|
||||
#' @param viewer Specify where the gadget should be displayed--viewer pane,
|
||||
#' dialog window, or external browser--by passing in a call to one of the
|
||||
#' [viewer()] functions.
|
||||
#' @param stopOnCancel If `TRUE` (the default), then an `observeEvent`
|
||||
#' is automatically created that handles `input$cancel` by calling
|
||||
#' `stopApp()` with an error. Pass `FALSE` if you want to handle
|
||||
#' `input$cancel` yourself.
|
||||
#' @return The value returned by the gadget.
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' library(shiny)
|
||||
#'
|
||||
#' ui <- fillPage(...)
|
||||
#'
|
||||
#' server <- function(input, output, session) {
|
||||
#' ...
|
||||
#' }
|
||||
#'
|
||||
#' # Either pass ui/server as separate arguments...
|
||||
#' runGadget(ui, server)
|
||||
#'
|
||||
#' # ...or as a single app object
|
||||
#' runGadget(shinyApp(ui, server))
|
||||
#' }
|
||||
#' @export
|
||||
runGadget <- function(app, server = NULL, port = getOption("shiny.port"),
|
||||
viewer = paneViewer(), stopOnCancel = TRUE) {
|
||||
|
||||
if (!is.shiny.appobj(app)) {
|
||||
app <- shinyApp(app, server)
|
||||
}
|
||||
|
||||
if (isTRUE(stopOnCancel)) {
|
||||
app <- decorateServerFunc(app, function(input, output, session) {
|
||||
observeEvent(input$cancel, {
|
||||
stopApp(stop("User cancel", call. = FALSE))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (is.null(viewer)) {
|
||||
viewer <- utils::browseURL
|
||||
}
|
||||
|
||||
shiny::runApp(app, port = port, launch.browser = viewer)
|
||||
}
|
||||
|
||||
# Add custom functionality to a Shiny app object's server func
|
||||
decorateServerFunc <- function(appobj, serverFunc) {
|
||||
origServerFuncSource <- appobj$serverFuncSource
|
||||
appobj$serverFuncSource <- function() {
|
||||
origServerFunc <- origServerFuncSource()
|
||||
function(input, output, session) {
|
||||
serverFunc(input, output, session)
|
||||
|
||||
# The clientData and session arguments are optional; check if
|
||||
# each exists
|
||||
args <- argsForServerFunc(origServerFunc, session)
|
||||
do.call(origServerFunc, args)
|
||||
}
|
||||
}
|
||||
appobj
|
||||
}
|
||||
|
||||
#' Viewer options
|
||||
#'
|
||||
#' Use these functions to control where the gadget is displayed in RStudio (or
|
||||
#' other R environments that emulate RStudio's viewer pane/dialog APIs). If
|
||||
#' viewer APIs are not available in the current R environment, then the gadget
|
||||
#' will be displayed in the system's default web browser (see
|
||||
#' [utils::browseURL()]).
|
||||
#'
|
||||
#' @return A function that takes a single `url` parameter, suitable for
|
||||
#' passing as the `viewer` argument of [runGadget()].
|
||||
#'
|
||||
#' @rdname viewer
|
||||
#' @name viewer
|
||||
NULL
|
||||
|
||||
#' @param minHeight The minimum height (in pixels) desired to show the gadget in
|
||||
#' the viewer pane. If a positive number, resize the pane if necessary to show
|
||||
#' at least that many pixels. If `NULL`, use the existing viewer pane
|
||||
#' size. If `"maximize"`, use the maximum available vertical space.
|
||||
#' @rdname viewer
|
||||
#' @export
|
||||
paneViewer <- function(minHeight = NULL) {
|
||||
viewer <- getOption("viewer")
|
||||
if (is.null(viewer)) {
|
||||
utils::browseURL
|
||||
} else {
|
||||
function(url) {
|
||||
viewer(url, minHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#' @param dialogName The window title to display for the dialog.
|
||||
#' @param width,height The desired dialog width/height, in pixels.
|
||||
#' @rdname viewer
|
||||
#' @export
|
||||
dialogViewer <- function(dialogName, width = 600, height = 600) {
|
||||
viewer <- getOption("shinygadgets.showdialog")
|
||||
if (is.null(viewer)) {
|
||||
utils::browseURL
|
||||
} else {
|
||||
function(url) {
|
||||
viewer(dialogName, url, width = width, height = height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#' @param browser See [utils::browseURL()].
|
||||
#' @rdname viewer
|
||||
#' @export
|
||||
browserViewer <- function(browser = getOption("browser")) {
|
||||
function(url) {
|
||||
utils::browseURL(url, browser = browser)
|
||||
}
|
||||
}
|
||||
|
||||
# Returns TRUE if we're running in Shiny Server or other hosting environment,
|
||||
# otherwise returns FALSE.
|
||||
|
||||
@@ -8,31 +8,55 @@ getShinyOption <- function(name, default = NULL) {
|
||||
# Make sure to use named (not numeric) indexing
|
||||
name <- as.character(name)
|
||||
|
||||
if (name %in% names(.globals$options))
|
||||
.globals$options[[name]]
|
||||
else
|
||||
default
|
||||
# Check if there's a current session
|
||||
session <- getDefaultReactiveDomain()
|
||||
if (!is.null(session)) {
|
||||
if (name %in% names(session$options)) {
|
||||
return(session$options[[name]])
|
||||
} else {
|
||||
return(default)
|
||||
}
|
||||
}
|
||||
|
||||
# Check if there's a current app
|
||||
app_state <- getCurrentAppState()
|
||||
if (!is.null(app_state)) {
|
||||
if (name %in% names(app_state$options)) {
|
||||
return(app_state$options[[name]])
|
||||
} else {
|
||||
return(default)
|
||||
}
|
||||
}
|
||||
|
||||
# If we got here, look in global options
|
||||
if (name %in% names(.globals$options)) {
|
||||
return(.globals$options[[name]])
|
||||
} else {
|
||||
return(default)
|
||||
}
|
||||
}
|
||||
|
||||
#' Get or set Shiny options
|
||||
#'
|
||||
#' `getShinyOption()` retrieves the value of a Shiny option. `shinyOptions()`
|
||||
#' sets the value of Shiny options; it can also be used to return a list of all
|
||||
#' currently-set Shiny options.
|
||||
#' @description
|
||||
#'
|
||||
#' @section Scope:
|
||||
#' There is a global option set which is available by default. When a Shiny
|
||||
#' application is run with [runApp()], that option set is duplicated and the
|
||||
#' new option set is available for getting or setting values. If options
|
||||
#' are set from `global.R`, `app.R`, `ui.R`, or `server.R`, or if they are set
|
||||
#' from inside the server function, then the options will be scoped to the
|
||||
#' application. When the application exits, the new option set is discarded and
|
||||
#' the global option set is restored.
|
||||
#' There are two mechanisms for working with options for Shiny. One is the
|
||||
#' [options()] function, which is part of base R, and the other is the
|
||||
#' `shinyOptions()` function, which is in the Shiny package. The reason for
|
||||
#' these two mechanisms is has to do with legacy code and scoping.
|
||||
#'
|
||||
#' @section Options:
|
||||
#' There are a number of global options that affect Shiny's behavior. These can
|
||||
#' be set globally with `options()` or locally (for a single app) with
|
||||
#' `shinyOptions()`.
|
||||
#' The [options()] function sets options globally, for the duration of the R
|
||||
#' process. The [getOption()] function retrieves the value of an option. All
|
||||
#' shiny related options of this type are prefixed with `"shiny."`.
|
||||
#'
|
||||
#' The `shinyOptions()` function sets the value of a shiny option, but unlike
|
||||
#' `options()`, it is not always global in scope; the options may be scoped
|
||||
#' globally, to an application, or to a user session in an application,
|
||||
#' depending on the context. The `getShinyOption()` function retrieves a value
|
||||
#' of a shiny option. Currently, the options set via `shinyOptions` are for
|
||||
#' internal use only.
|
||||
#'
|
||||
#' @section Options with `options()`:
|
||||
#'
|
||||
#' \describe{
|
||||
#' \item{shiny.autoreload (defaults to `FALSE`)}{If `TRUE` when a Shiny app is launched, the
|
||||
@@ -64,8 +88,8 @@ getShinyOption <- function(name, default = NULL) {
|
||||
#' \item{shiny.host (defaults to `"127.0.0.1"`)}{The IP address that Shiny should listen on. See
|
||||
#' [runApp()] for more information.}
|
||||
#' \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`,
|
||||
#' then jQuery 3.5.1 is used.}
|
||||
#' 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.}
|
||||
#' \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.}
|
||||
#' \item{shiny.launch.browser (defaults to `interactive()`)}{A boolean which controls the default behavior
|
||||
@@ -107,46 +131,126 @@ getShinyOption <- function(name, default = NULL) {
|
||||
#' `"recv"` (only print messages received by the server), `TRUE`
|
||||
#' (print all messages), or `FALSE` (default; don't print any of these
|
||||
#' messages).}
|
||||
#' \item{shiny.autoload.r (defaults to `TRUE`)}{If `TRUE`, then the R/
|
||||
#' of a shiny app will automatically be sourced.}
|
||||
#' \item{shiny.usecairo (defaults to `TRUE`)}{This is used to disable graphical rendering by the
|
||||
#' Cairo package, if it is installed. See [plotPNG()] for more
|
||||
#' information.}
|
||||
#' \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.}
|
||||
### Not documenting as 'shiny.devmode.verbose' is for niche use only
|
||||
# ' \item{shiny.devmode.verbose (defaults to `TRUE`)}{If `TRUE`, will display messages printed
|
||||
# ' about which options are being set. See [devmode()] for more details. }
|
||||
### (end not documenting 'shiny.devmode.verbose')
|
||||
#' }
|
||||
#'
|
||||
#'
|
||||
#' @section Scoping for `shinyOptions()`:
|
||||
#'
|
||||
#' There are three levels of scoping for `shinyOptions()`: global,
|
||||
#' application, and session.
|
||||
#'
|
||||
#' The global option set is available by default. Any calls to
|
||||
#' `shinyOptions()` and `getShinyOption()` outside of an app will access the
|
||||
#' global option set.
|
||||
#'
|
||||
#' When a Shiny application is run with [runApp()], the global option set is
|
||||
#' duplicated and the new option set is available at the application level. If
|
||||
#' options are set from `global.R`, `app.R`, `ui.R`, or `server.R` (but
|
||||
#' outside of the server function), then the application-level options will be
|
||||
#' modified.
|
||||
#'
|
||||
#' Each time a user session is started, the application-level option set is
|
||||
#' duplicated, for that session. If the options are set from inside the server
|
||||
#' function, then they will be scoped to the session.
|
||||
#'
|
||||
#' @section Options with `shinyOptions()`:
|
||||
#'
|
||||
#' There are a number of global options that affect Shiny's behavior. These
|
||||
#' can be set globally with `options()` or locally (for a single app) with
|
||||
#' `shinyOptions()`.
|
||||
#'
|
||||
#' \describe{ \item{cache}{A caching object that will be used by
|
||||
#' [renderCachedPlot()]. If not specified, a [cachem::cache_mem()] will be
|
||||
#' used.} }
|
||||
#'
|
||||
#' @param ... Options to set, with the form `name = value`.
|
||||
#' @aliases shiny-options
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#' shinyOptions(myOption = 10)
|
||||
#' getShinyOption("myOption")
|
||||
#' }
|
||||
#' @export
|
||||
shinyOptions <- function(...) {
|
||||
newOpts <- list(...)
|
||||
|
||||
if (length(newOpts) > 0) {
|
||||
# If we're within a session, modify at the session level.
|
||||
session <- getDefaultReactiveDomain()
|
||||
if (!is.null(session)) {
|
||||
# Modify session-level-options
|
||||
session$options <- dropNulls(mergeVectors(session$options, newOpts))
|
||||
return(invisible(session$options))
|
||||
}
|
||||
|
||||
# If not in a session, but we have a currently running app, modify options
|
||||
# at the app level.
|
||||
app_state <- getCurrentAppState()
|
||||
if (!is.null(app_state)) {
|
||||
# Modify app-level options
|
||||
app_state$options <- dropNulls(mergeVectors(app_state$options, newOpts))
|
||||
return(invisible(app_state$options))
|
||||
}
|
||||
|
||||
# If no currently running app, modify global options and return them.
|
||||
.globals$options <- dropNulls(mergeVectors(.globals$options, newOpts))
|
||||
invisible(.globals$options)
|
||||
} else {
|
||||
.globals$options
|
||||
return(invisible(.globals$options))
|
||||
}
|
||||
}
|
||||
|
||||
# If not setting any options, just return current option set, visibly.
|
||||
|
||||
# Eval an expression with a new option set
|
||||
withLocalOptions <- function(expr) {
|
||||
oldOptionSet <- .globals$options
|
||||
on.exit(.globals$options <- oldOptionSet)
|
||||
session <- getDefaultReactiveDomain()
|
||||
if (!is.null(session)) {
|
||||
return(session$options)
|
||||
}
|
||||
|
||||
expr
|
||||
app_state <- getCurrentAppState()
|
||||
if (!is.null(app_state)) {
|
||||
return(app_state$options)
|
||||
}
|
||||
|
||||
return(.globals$options)
|
||||
}
|
||||
|
||||
|
||||
# Get specific shiny options and put them in a list, reset those shiny options,
|
||||
# and then return the options list. This should be during the creation of a
|
||||
# shiny app object, which happens before another option frame is added to the
|
||||
# options stack (the new option frame is added when the app is run). This
|
||||
# function "consumes" the options when the shinyApp object is created, so the
|
||||
# options won't affect another app that is created later.
|
||||
consumeAppOptions <- function() {
|
||||
# shiny app object. This function "consumes" the options when the shinyApp
|
||||
# object is created, so the options won't affect another app that is created
|
||||
# later.
|
||||
#
|
||||
# ==== Example ====
|
||||
# shinyOptions(bookmarkStore = 1234)
|
||||
# # This now returns 1234.
|
||||
# getShinyOption("bookmarkStore")
|
||||
#
|
||||
# # Creating the app captures the bookmarkStore option and clears it.
|
||||
# s <- shinyApp(
|
||||
# fluidPage(verbatimTextOutput("txt")),
|
||||
# function(input, output) {
|
||||
# output$txt <- renderText(getShinyOption("bookmarkStore"))
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# # This now returns NULL.
|
||||
# getShinyOption("bookmarkStore")
|
||||
#
|
||||
# When running the app, the app will display "1234"
|
||||
# runApp(s)
|
||||
#
|
||||
# # After quitting the app, this still returns NULL.
|
||||
# getShinyOption("bookmarkStore")
|
||||
# ==================
|
||||
#
|
||||
# If another app had been created after s was created, but before s was run,
|
||||
# then it would capture the value of "bookmarkStore" at the time of creation.
|
||||
captureAppOptions <- function() {
|
||||
options <- list(
|
||||
appDir = getwd(),
|
||||
bookmarkStore = getShinyOption("bookmarkStore")
|
||||
@@ -157,9 +261,9 @@ consumeAppOptions <- function() {
|
||||
options
|
||||
}
|
||||
|
||||
# Do the inverse of consumeAppOptions. This should be called once the app is
|
||||
# Do the inverse of captureAppOptions. This should be called once the app is
|
||||
# started.
|
||||
unconsumeAppOptions <- function(options) {
|
||||
applyCapturedAppOptions <- function(options) {
|
||||
if (!is.null(options)) {
|
||||
do.call(shinyOptions, options)
|
||||
}
|
||||
|
||||
34
R/shiny-package.R
Normal file
34
R/shiny-package.R
Normal file
@@ -0,0 +1,34 @@
|
||||
# See also R/reexports.R
|
||||
|
||||
## usethis namespace: start
|
||||
## usethis namespace: end
|
||||
#' @importFrom lifecycle deprecated
|
||||
#' @importFrom grDevices dev.set dev.cur
|
||||
#' @importFrom fastmap fastmap
|
||||
#' @importFrom promises %...!%
|
||||
#' @importFrom promises %...>%
|
||||
#' @importFrom promises
|
||||
#' promise promise_resolve promise_reject is.promising
|
||||
#' as.promise
|
||||
#' @importFrom rlang
|
||||
#' quo enquo as_function get_expr get_env new_function enquos
|
||||
#' eval_tidy expr pairlist2 new_quosure enexpr as_quosure is_quosure inject
|
||||
#' enquos0 zap_srcref %||% is_na
|
||||
#' is_false
|
||||
#' missing_arg is_missing maybe_missing
|
||||
#' @importFrom ellipsis
|
||||
#' check_dots_empty check_dots_unnamed
|
||||
#' @import htmltools
|
||||
#' @import httpuv
|
||||
#' @import xtable
|
||||
#' @import R6
|
||||
#' @import mime
|
||||
NULL
|
||||
|
||||
# It's necessary to Depend on methods so Rscript doesn't fail. It's necessary
|
||||
# to import(methods) in NAMESPACE so R CMD check doesn't complain. This
|
||||
# approach isn't foolproof because Rscript -e pkgname::func() doesn't actually
|
||||
# cause methods to be attached, but it's not a problem for shiny::runApp()
|
||||
# since we call require(shiny) as part of loading the app.
|
||||
#' @import methods
|
||||
NULL
|
||||
412
R/shiny.R
412
R/shiny.R
@@ -1,4 +1,4 @@
|
||||
#' @include utils.R stack.R
|
||||
#' @include utils.R
|
||||
NULL
|
||||
|
||||
#' Web Application Framework for R
|
||||
@@ -8,7 +8,7 @@ NULL
|
||||
#' prebuilt widgets make it possible to build beautiful, responsive, and
|
||||
#' powerful applications with minimal effort.
|
||||
#'
|
||||
#' The Shiny tutorial at <http://shiny.rstudio.com/tutorial/> explains
|
||||
#' The Shiny tutorial at <https://shiny.rstudio.com/tutorial/> explains
|
||||
#' the framework in depth, walks you through building a simple application, and
|
||||
#' includes extensive annotated examples.
|
||||
#'
|
||||
@@ -17,15 +17,6 @@ NULL
|
||||
#' @name shiny-package
|
||||
#' @aliases shiny
|
||||
#' @docType package
|
||||
#' @import htmltools httpuv xtable digest R6 mime
|
||||
NULL
|
||||
|
||||
# It's necessary to Depend on methods so Rscript doesn't fail. It's necessary
|
||||
# to import(methods) in NAMESPACE so R CMD check doesn't complain. This
|
||||
# approach isn't foolproof because Rscript -e pkgname::func() doesn't actually
|
||||
# cause methods to be attached, but it's not a problem for shiny::runApp()
|
||||
# since we call require(shiny) as part of loading the app.
|
||||
#' @import methods
|
||||
NULL
|
||||
|
||||
createUniqueId <- function(bytes, prefix = "", suffix = "") {
|
||||
@@ -117,9 +108,6 @@ workerId <- local({
|
||||
#' \item{clientData}{
|
||||
#' A [reactiveValues()] object that contains information about the client.
|
||||
#' \itemize{
|
||||
#' \item{`allowDataUriScheme` is a logical value that indicates whether
|
||||
#' the browser is able to handle URIs that use the `data:` scheme.
|
||||
#' }
|
||||
#' \item{`pixelratio` reports the "device pixel ratio" from the web browser,
|
||||
#' or 1 if none is reported. The value is 2 for Apple Retina displays.
|
||||
#' }
|
||||
@@ -205,6 +193,14 @@ workerId <- local({
|
||||
#' An environment for app authors and module/package authors to store whatever
|
||||
#' session-specific data they want.
|
||||
#' }
|
||||
#' \item{user}{
|
||||
#' User's log-in information. Useful for identifying users on hosted platforms
|
||||
#' such as RStudio Connect and Shiny Server.
|
||||
#' }
|
||||
#' \item{groups}{
|
||||
#' The `user`'s relevant group information. Useful for determining what
|
||||
#' privileges the user should or shouldn't have.
|
||||
#' }
|
||||
#' \item{resetBrush(brushId)}{
|
||||
#' Resets/clears the brush with the given `brushId`, if it exists on
|
||||
#' any `imageOutput` or `plotOutput` in the app.
|
||||
@@ -272,6 +268,18 @@ workerId <- local({
|
||||
#' character vector, as in `input=c("x", "y")`. The format can be
|
||||
#' "rds" or "json".
|
||||
#' }
|
||||
#' \item{setCurrentTheme(theme)}{
|
||||
#' Sets the current [bootstrapLib()] theme, which updates the value of
|
||||
#' [getCurrentTheme()], invalidates `session$getCurrentTheme()`, and calls
|
||||
#' function(s) registered with [registerThemeDependency()] with provided
|
||||
#' `theme`. If those function calls return [htmltools::htmlDependency()]s with
|
||||
#' `stylesheet`s, then those stylesheets are "refreshed" (i.e., the new
|
||||
#' stylesheets are inserted on the page and the old ones are disabled and
|
||||
#' removed).
|
||||
#' }
|
||||
#' \item{getCurrentTheme()}{
|
||||
#' A reactive read of the current [bootstrapLib()] theme.
|
||||
#' }
|
||||
#'
|
||||
#' @name session
|
||||
NULL
|
||||
@@ -280,7 +288,7 @@ NULL
|
||||
#'
|
||||
#' The `NS` function creates namespaced IDs out of bare IDs, by joining
|
||||
#' them using `ns.sep` as the delimiter. It is intended for use in Shiny
|
||||
#' modules. See <http://shiny.rstudio.com/articles/modules.html>.
|
||||
#' modules. See <https://shiny.rstudio.com/articles/modules.html>.
|
||||
#'
|
||||
#' Shiny applications use IDs to identify inputs and outputs. These IDs must be
|
||||
#' unique within an application, as accidentally using the same input/output ID
|
||||
@@ -297,7 +305,7 @@ NULL
|
||||
#' @param id The id string to be namespaced (optional).
|
||||
#' @return If `id` is missing, returns a function that expects an id string
|
||||
#' as its only argument and returns that id with the namespace prepended.
|
||||
#' @seealso <http://shiny.rstudio.com/articles/modules.html>
|
||||
#' @seealso <https://shiny.rstudio.com/articles/modules.html>
|
||||
#' @export
|
||||
NS <- function(namespace, id = NULL) {
|
||||
if (length(namespace) == 0)
|
||||
@@ -335,8 +343,8 @@ ShinySession <- R6Class(
|
||||
websocket = 'ANY',
|
||||
invalidatedOutputValues = 'Map',
|
||||
invalidatedOutputErrors = 'Map',
|
||||
inputMessageQueue = list(), # A list of inputMessages to send when flushed
|
||||
cycleStartActionQueue = list(), # A list of actions to perform to start a cycle
|
||||
inputMessageQueue = 'fastqueue', # A list of inputMessages to send when flushed
|
||||
cycleStartActionQueue = 'fastqueue', # A list of actions to perform to start a cycle
|
||||
.outputs = list(), # Keeps track of all the output observer objects
|
||||
.outputOptions = list(), # Options for each of the output observer objects
|
||||
progressKeys = 'character',
|
||||
@@ -363,6 +371,7 @@ ShinySession <- R6Class(
|
||||
currentOutputName = NULL, # Name of the currently-running output
|
||||
outputInfo = list(), # List of information for each output
|
||||
testSnapshotUrl = character(0),
|
||||
currentThemeDependency = NULL, # ReactiveVal for taking dependency on theme
|
||||
|
||||
sendResponse = function(requestMsg, value) {
|
||||
if (is.null(requestMsg$tag)) {
|
||||
@@ -468,7 +477,7 @@ ShinySession <- R6Class(
|
||||
# The format of the response that will be sent back. Defaults to
|
||||
# "json" unless requested otherwise. The only other valid value is
|
||||
# "rds".
|
||||
format <- params$format %OR% "json"
|
||||
format <- params$format %||% "json"
|
||||
|
||||
values <- list()
|
||||
|
||||
@@ -599,23 +608,22 @@ ShinySession <- R6Class(
|
||||
# function has been set, return the identity function.
|
||||
getSnapshotPreprocessOutput = function(name) {
|
||||
fun <- attr(private$.outputs[[name]], "snapshotPreprocess", exact = TRUE)
|
||||
fun %OR% identity
|
||||
fun %||% identity
|
||||
},
|
||||
|
||||
# Get the snapshotPreprocessInput function for an input name. If no preprocess
|
||||
# function has been set, return the identity function.
|
||||
getSnapshotPreprocessInput = function(name) {
|
||||
fun <- private$.input$getMeta(name, "shiny.snapshot.preprocess")
|
||||
fun %OR% identity
|
||||
fun %||% identity
|
||||
},
|
||||
|
||||
# See cycleStartAction
|
||||
startCycle = function() {
|
||||
# TODO: This should check for busyCount == 0L, and remove the checks from
|
||||
# the call sites
|
||||
if (length(private$cycleStartActionQueue) > 0) {
|
||||
head <- private$cycleStartActionQueue[[1L]]
|
||||
private$cycleStartActionQueue <- private$cycleStartActionQueue[-1L]
|
||||
if (private$cycleStartActionQueue$size() > 0) {
|
||||
head <- private$cycleStartActionQueue$remove()
|
||||
|
||||
# After we execute the current cycleStartAction (head), there may be
|
||||
# more items left on the queue. If the current busyCount > 0, then that
|
||||
@@ -634,7 +642,7 @@ ShinySession <- R6Class(
|
||||
# busyCount, it's possible we're calling startCycle spuriously; that's
|
||||
# OK, it's essentially a no-op in that case.
|
||||
on.exit({
|
||||
if (private$busyCount == 0L && length(private$cycleStartActionQueue) > 0L) {
|
||||
if (private$busyCount == 0L && private$cycleStartActionQueue$size() > 0L) {
|
||||
later::later(function() {
|
||||
if (private$busyCount == 0L) {
|
||||
private$startCycle()
|
||||
@@ -665,12 +673,15 @@ ShinySession <- R6Class(
|
||||
cache = NULL, # A cache object used in the session
|
||||
user = NULL,
|
||||
groups = NULL,
|
||||
options = NULL, # For session-specific shinyOptions()
|
||||
|
||||
initialize = function(websocket) {
|
||||
private$websocket <- websocket
|
||||
self$closed <- FALSE
|
||||
# TODO: Put file upload context in user/app-specific dir if possible
|
||||
|
||||
private$inputMessageQueue <- fastmap::fastqueue()
|
||||
private$cycleStartActionQueue <- fastmap::fastqueue()
|
||||
private$invalidatedOutputValues <- Map$new()
|
||||
private$invalidatedOutputErrors <- Map$new()
|
||||
private$fileUploadContext <- FileUploadContext$new()
|
||||
@@ -681,7 +692,7 @@ ShinySession <- R6Class(
|
||||
private$.input <- ReactiveValues$new(dedupe = FALSE, label = "input")
|
||||
private$.clientData <- ReactiveValues$new(dedupe = TRUE, label = "clientData")
|
||||
private$timingRecorder <- ShinyServerTimingRecorder$new()
|
||||
self$progressStack <- Stack$new()
|
||||
self$progressStack <- fastmap::faststack()
|
||||
self$files <- Map$new()
|
||||
self$downloads <- Map$new()
|
||||
self$userData <- new.env(parent = emptyenv())
|
||||
@@ -695,16 +706,26 @@ ShinySession <- R6Class(
|
||||
private$.outputs <- list()
|
||||
private$.outputOptions <- list()
|
||||
|
||||
self$cache <- MemoryCache$new()
|
||||
# Copy app-level options
|
||||
self$options <- getCurrentAppState()$options
|
||||
|
||||
self$cache <- cachem::cache_mem(max_size = 200 * 1024^2)
|
||||
|
||||
private$bookmarkCallbacks <- Callbacks$new()
|
||||
private$bookmarkedCallbacks <- Callbacks$new()
|
||||
private$restoreCallbacks <- Callbacks$new()
|
||||
private$restoredCallbacks <- Callbacks$new()
|
||||
|
||||
private$testMode <- .globals$testMode
|
||||
private$testMode <- getShinyOption("testmode", default = FALSE)
|
||||
private$enableTestSnapshot()
|
||||
|
||||
# This `withReactiveDomain` is used only to satisfy the reactlog, so that
|
||||
# it knows to scope this reactiveVal to this session.
|
||||
# https://github.com/rstudio/shiny/pull/3182
|
||||
withReactiveDomain(self,
|
||||
private$currentThemeDependency <- reactiveVal(0, label = "Theme Counter")
|
||||
)
|
||||
|
||||
private$registerSessionEndCallbacks()
|
||||
|
||||
if (!is.null(websocket$request$HTTP_SHINY_SERVER_CREDENTIALS)) {
|
||||
@@ -942,7 +963,33 @@ ShinySession <- R6Class(
|
||||
|
||||
impl <- .subset2(x, 'impl')
|
||||
key <- .subset2(x, 'ns')(name)
|
||||
impl$freeze(key)
|
||||
|
||||
is_input <- identical(impl, private$.input)
|
||||
|
||||
# There's no good reason for us not to just do force=TRUE, except that we
|
||||
# know this fixes problems for freezeReactiveValue(input) but we don't
|
||||
# currently even know what you would use freezeReactiveValue(rv) for. In
|
||||
# the spirit of not breaking things we don't understand, we're making as
|
||||
# targeted a fix as possible, while emitting a deprecation warning (below)
|
||||
# that should help us gather more data about the other case.
|
||||
impl$freeze(key, invalidate = is_input)
|
||||
|
||||
if (is_input) {
|
||||
# Notify the client that this input was frozen. The client will ensure
|
||||
# that the next time it sees a value for that input, even if the value
|
||||
# has not changed from the last known value of that input, it will be
|
||||
# sent to the server anyway.
|
||||
private$sendMessage(frozen = list(
|
||||
ids = list(key)
|
||||
))
|
||||
} else {
|
||||
if (getOption("shiny.deprecation.messages", TRUE) && getOption("shiny.deprecation.messages.freeze", TRUE)) {
|
||||
rlang::warn(
|
||||
"Support for calling freezeReactiveValue() with non-`input` reactiveValues objects is soft-deprecated, and may be removed in a future version of Shiny. (See https://github.com/rstudio/shiny/issues/3063)",
|
||||
.frequency = "once", .frequency_id = "freezeReactiveValue")
|
||||
}
|
||||
}
|
||||
|
||||
self$onFlushed(function() impl$thaw(key))
|
||||
},
|
||||
|
||||
@@ -1131,7 +1178,10 @@ ShinySession <- R6Class(
|
||||
private$.outputOptions[[name]] <- list()
|
||||
}
|
||||
else {
|
||||
stop(paste("Unexpected", class(func), "output for", name))
|
||||
rlang::abort(c(
|
||||
paste0("Unexpected ", class(func)[[1]], " object for output$", name),
|
||||
i = "Did you forget to use a render function?"
|
||||
))
|
||||
}
|
||||
},
|
||||
getOutput = function(name) {
|
||||
@@ -1161,7 +1211,7 @@ ShinySession <- R6Class(
|
||||
length(private$progressKeys) != 0 ||
|
||||
length(private$invalidatedOutputValues) != 0 ||
|
||||
length(private$invalidatedOutputErrors) != 0 ||
|
||||
length(private$inputMessageQueue) != 0
|
||||
private$inputMessageQueue$size() != 0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1193,8 +1243,8 @@ ShinySession <- R6Class(
|
||||
private$invalidatedOutputValues <- Map$new()
|
||||
errors <- as.list(private$invalidatedOutputErrors)
|
||||
private$invalidatedOutputErrors <- Map$new()
|
||||
inputMessages <- private$inputMessageQueue
|
||||
private$inputMessageQueue <- list()
|
||||
inputMessages <- private$inputMessageQueue$as_list()
|
||||
private$inputMessageQueue$reset()
|
||||
|
||||
if (isTRUE(private$testMode)) {
|
||||
private$storeOutputValues(mergeVectors(values, errors))
|
||||
@@ -1212,7 +1262,7 @@ ShinySession <- R6Class(
|
||||
# does not guarantee) inputs and reactive values from changing underneath
|
||||
# async observers as they run.
|
||||
cycleStartAction = function(callback) {
|
||||
private$cycleStartActionQueue <- c(private$cycleStartActionQueue, list(callback))
|
||||
private$cycleStartActionQueue$add(callback)
|
||||
# If no observers are running in this session, we're safe to proceed.
|
||||
# Otherwise, startCycle() will be called later, via decrementBusyCount().
|
||||
if (private$busyCount == 0L) {
|
||||
@@ -1252,6 +1302,66 @@ ShinySession <- R6Class(
|
||||
modal = list(type = type, message = message)
|
||||
)
|
||||
},
|
||||
|
||||
getCurrentTheme = function() {
|
||||
private$currentThemeDependency()
|
||||
getCurrentTheme()
|
||||
},
|
||||
|
||||
setCurrentTheme = function(theme) {
|
||||
# This function does three things: (1) sets theme as the current
|
||||
# bootstrapTheme, (2) re-executes any registered theme dependencies, and
|
||||
# (3) sends the resulting dependencies to the client.
|
||||
|
||||
if (!is_bs_theme(theme)) {
|
||||
stop("`session$setCurrentTheme()` expects a `bslib::bs_theme()` object.", call. = FALSE)
|
||||
}
|
||||
|
||||
# Switching Bootstrap versions has weird & complex consequences
|
||||
# for the JS logic, so we forbid it
|
||||
current_version <- bslib::theme_version(getCurrentTheme())
|
||||
next_version <- bslib::theme_version(theme)
|
||||
if (!identical(current_version, next_version)) {
|
||||
stop(
|
||||
"session$setCurrentTheme() cannot be used to change the Bootstrap version ",
|
||||
"from ", current_version, " to ", next_version, ". ",
|
||||
"Try using `bs_theme(version = ", next_version, ")` for initial theme.",
|
||||
call. = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
# Note that this will automatically scope to the session.
|
||||
setCurrentTheme(theme)
|
||||
|
||||
# Invalidate
|
||||
private$currentThemeDependency(isolate(private$currentThemeDependency()) + 1)
|
||||
|
||||
# Call any theme dependency functions and make sure we get a list of deps back
|
||||
funcs <- getShinyOption("themeDependencyFuncs", default = list())
|
||||
deps <- lapply(funcs, function(func) {
|
||||
deps <- func(theme)
|
||||
if (length(deps) == 0) return(NULL)
|
||||
if (inherits(deps, "html_dependency")) return(list(deps))
|
||||
is_dep <- vapply(deps, inherits, logical(1), "html_dependency")
|
||||
if (all(is_dep)) return(deps)
|
||||
stop("All registerThemeDependency() functions must yield htmlDependency() object(s)", call. = FALSE)
|
||||
})
|
||||
# Work with a flat list of dependencies
|
||||
deps <- unlist(dropNulls(deps), recursive = FALSE)
|
||||
# Add a special flag to let Shiny.renderDependencies() know that, even
|
||||
# though we've already rendered the dependency, that we need to re-render
|
||||
# the stylesheets
|
||||
deps <- lapply(deps, function(dep) {
|
||||
dep$restyle <- TRUE
|
||||
dep
|
||||
})
|
||||
|
||||
# Send any dependencies to be re-rendered
|
||||
if (length(deps)) {
|
||||
insertUI(selector = "body", where = "afterEnd", ui = tagList(deps))
|
||||
}
|
||||
},
|
||||
|
||||
dispatch = function(msg) {
|
||||
method <- paste('@', msg$method, sep='')
|
||||
func <- try(self[[method]], silent = TRUE)
|
||||
@@ -1283,8 +1393,7 @@ ShinySession <- R6Class(
|
||||
sendInputMessage = function(inputId, message) {
|
||||
data <- list(id = inputId, message = message)
|
||||
|
||||
# Add to input message queue
|
||||
private$inputMessageQueue[[length(private$inputMessageQueue) + 1]] <- data
|
||||
private$inputMessageQueue$add(data)
|
||||
# Needed so that Shiny knows to actually flush the input message queue
|
||||
self$requestFlush()
|
||||
},
|
||||
@@ -1317,82 +1426,97 @@ ShinySession <- R6Class(
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
tmp_info <- private$outputInfo[[name]] %OR% list(name = name)
|
||||
if (!is.null(private$outputInfo[[name]])) {
|
||||
return(private$outputInfo[[name]])
|
||||
}
|
||||
|
||||
# The following code will only run the first time this function has been
|
||||
# called for this output.
|
||||
|
||||
tmp_info <- list(name = name)
|
||||
|
||||
# cd_names() returns names of all items in clientData, without taking a
|
||||
# reactive dependency. It is a function and it's memoized, so that we do
|
||||
# the (relatively) expensive isolate(names(...)) call only when needed,
|
||||
# and at most one time in this function.
|
||||
.cd_names <- NULL
|
||||
cd_names <- function() {
|
||||
if (is.null(.cd_names)) {
|
||||
.cd_names <<- isolate(names(self$clientData))
|
||||
}
|
||||
.cd_names
|
||||
}
|
||||
|
||||
# If we don't already have width for this output info, see if it's
|
||||
# present, and if so, add it.
|
||||
|
||||
# Note that all the following clientData values (which are reactiveValues)
|
||||
# are wrapped in reactive() so that users can take a dependency on particular
|
||||
# output info (i.e., just depend on width/height, or just depend on bg, fg, etc).
|
||||
# To put it another way, if getCurrentOutputInfo() simply returned a list of values
|
||||
# from self$clientData, than anything that calls getCurrentOutputInfo() would take
|
||||
# a reactive dependency on all of these values.
|
||||
if (! ("width" %in% names(tmp_info)) ) {
|
||||
width_name <- paste0("output_", name, "_width")
|
||||
if (width_name %in% cd_names()) {
|
||||
tmp_info$width <- reactive({
|
||||
self$clientData[[width_name]]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (! ("height" %in% names(tmp_info)) ) {
|
||||
height_name <- paste0("output_", name, "_height")
|
||||
if (height_name %in% cd_names()) {
|
||||
tmp_info$height <- reactive({
|
||||
self$clientData[[height_name]]
|
||||
})
|
||||
}
|
||||
}
|
||||
cd_names <- isolate(names(self$clientData))
|
||||
|
||||
# parseCssColors() currently errors out if you hand it any NAs
|
||||
# This'll make sure we're always working with a string (and if
|
||||
# that string isn't a valid CSS color, will return NA)
|
||||
# https://github.com/rstudio/htmltools/issues/161
|
||||
parse_css_colors <- function(x) {
|
||||
htmltools::parseCssColors(x %OR% "", mustWork = FALSE)
|
||||
htmltools::parseCssColors(x %||% "", mustWork = FALSE)
|
||||
}
|
||||
|
||||
bg <- paste0("output_", name, "_bg")
|
||||
if (bg %in% cd_names()) {
|
||||
tmp_info$bg <- reactive({
|
||||
parse_css_colors(self$clientData[[bg]])
|
||||
})
|
||||
|
||||
# This function conditionally adds an item to tmp_info (for "width", it
|
||||
# would create tmp_info$width). It is added _if_ there is an entry in
|
||||
# clientData like "output_foo_width", where "foo" is the name of the
|
||||
# output. The first time `tmp_info$width()` is called, it creates a
|
||||
# reactive expression that reads `clientData$output_foo_width`, saves it,
|
||||
# then invokes that reactive. On subsequent calls, the reactive already
|
||||
# exists, so it simply invokes it.
|
||||
#
|
||||
# The reason it creates the reactive only on first use is so that it
|
||||
# doesn't spuriously create reactives.
|
||||
#
|
||||
# This function essentially generalizes the code below for names other
|
||||
# than just "width".
|
||||
#
|
||||
# width_name <- paste0("output_", name, "_width")
|
||||
# if (width_name %in% cd_names()) {
|
||||
# width_r <- NULL
|
||||
# tmp_info$width <- function() {
|
||||
# if (is.null(width_r)) {
|
||||
# width_r <<- reactive({
|
||||
# parse_css_colors(self$clientData[[width_name]])
|
||||
# })
|
||||
# }
|
||||
#
|
||||
# width_r()
|
||||
# }
|
||||
# }
|
||||
add_conditional_reactive <- function(prop, wrapfun = identity) {
|
||||
force(prop)
|
||||
force(wrapfun)
|
||||
|
||||
prop_name <- paste0("output_", name, "_", prop)
|
||||
|
||||
# Only add tmp_info$width if clientData has "output_foo_width"
|
||||
if (prop_name %in% cd_names) {
|
||||
r <- NULL
|
||||
|
||||
# Turn it into a function that creates a reactive on the first
|
||||
# invocation of getCurrentOutputInfo()$width() and saves it; future
|
||||
# invocations of getCurrentOutputInfo()$width() use the existing
|
||||
# reactive and save it.
|
||||
tmp_info[[prop]] <<- function() {
|
||||
if (is.null(r)) {
|
||||
r <<- reactive(label = prop_name, {
|
||||
wrapfun(self$clientData[[prop_name]])
|
||||
})
|
||||
}
|
||||
|
||||
r()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fg <- paste0("output_", name, "_fg")
|
||||
if (fg %in% cd_names()) {
|
||||
tmp_info$fg <- reactive({
|
||||
parse_css_colors(self$clientData[[fg]])
|
||||
})
|
||||
}
|
||||
|
||||
accent <- paste0("output_", name, "_accent")
|
||||
if (accent %in% cd_names()) {
|
||||
tmp_info$accent <- reactive({
|
||||
parse_css_colors(self$clientData[[accent]])
|
||||
})
|
||||
}
|
||||
|
||||
font <- paste0("output_", name, "_font")
|
||||
if (font %in% cd_names()) {
|
||||
tmp_info$font <- reactive({
|
||||
self$clientData[[font]]
|
||||
})
|
||||
}
|
||||
# Note that all the following clientData values (which are reactiveValues)
|
||||
# are wrapped in reactive() so that users can take a dependency on
|
||||
# particular output info (i.e., just depend on width/height, or just
|
||||
# depend on bg, fg, etc). To put it another way, if getCurrentOutputInfo()
|
||||
# simply returned a list of values from self$clientData, than anything
|
||||
# that calls getCurrentOutputInfo() would take a reactive dependency on
|
||||
# all of these values.
|
||||
add_conditional_reactive("width")
|
||||
add_conditional_reactive("height")
|
||||
add_conditional_reactive("bg", parse_css_colors)
|
||||
add_conditional_reactive("fg", parse_css_colors)
|
||||
add_conditional_reactive("accent", parse_css_colors)
|
||||
add_conditional_reactive("font")
|
||||
|
||||
private$outputInfo[[name]] <- tmp_info
|
||||
private$outputInfo[[name]]
|
||||
@@ -1409,7 +1533,7 @@ ShinySession <- R6Class(
|
||||
# Warn if trying to enable save-to-server bookmarking on a version of SS,
|
||||
# SSP, or Connect that doesn't support it.
|
||||
if (store == "server" && inShinyServer() &&
|
||||
is.null(getShinyOption("save.interface")))
|
||||
is.null(getShinyOption("save.interface", default = NULL)))
|
||||
{
|
||||
showNotification(
|
||||
"This app tried to enable saved-to-server bookmarking, but it is not supported by the hosting environment.",
|
||||
@@ -1685,10 +1809,6 @@ ShinySession <- R6Class(
|
||||
)
|
||||
},
|
||||
|
||||
# Public RPC methods
|
||||
`@uploadieFinish` = function() {
|
||||
# Do nothing; just want the side effect of flushReact, output flush, etc.
|
||||
},
|
||||
`@uploadInit` = function(fileInfos) {
|
||||
maxSize <- getOption('shiny.maxRequestSize', 5 * 1024 * 1024)
|
||||
fileInfos <- lapply(fileInfos, function(fi) {
|
||||
@@ -1755,33 +1875,6 @@ ShinySession <- R6Class(
|
||||
}
|
||||
}
|
||||
|
||||
# @description Only applicable to files uploaded via IE. When possible,
|
||||
# adds the appropriate extension to temporary files created by
|
||||
# \code{mime::parse_multipart}.
|
||||
# @param multipart A named list as returned by
|
||||
# \code{mime::parse_multipart}
|
||||
# @return A named list with datapath updated to point to the new location
|
||||
# of the file, if an extension was added.
|
||||
maybeMoveIEUpload <- function(multipart) {
|
||||
if (is.null(multipart)) return(NULL)
|
||||
|
||||
lapply(multipart, function(input) {
|
||||
oldPath <- input$datapath
|
||||
newPath <- paste0(oldPath, maybeGetExtension(input$name))
|
||||
if (oldPath != newPath) {
|
||||
file.rename(oldPath, newPath)
|
||||
input$datapath <- newPath
|
||||
}
|
||||
input
|
||||
})
|
||||
}
|
||||
|
||||
if (matches[2] == 'uploadie' && identical(req$REQUEST_METHOD, "POST")) {
|
||||
id <- URLdecode(matches[3])
|
||||
res <- maybeMoveIEUpload(mime::parse_multipart(req))
|
||||
private$.input$set(id, res[[id]])
|
||||
return(httpResponse(200, 'text/plain', 'OK'))
|
||||
}
|
||||
|
||||
if (matches[2] == 'download') {
|
||||
|
||||
@@ -1856,15 +1949,17 @@ ShinySession <- R6Class(
|
||||
}
|
||||
return(httpResponse(
|
||||
200,
|
||||
download$contentType %OR% getContentType(filename),
|
||||
download$contentType %||% getContentType(filename),
|
||||
# owned=TRUE means tmpdata will be deleted after response completes
|
||||
list(file=tmpdata, owned=TRUE),
|
||||
c(
|
||||
'Content-Disposition' = ifelse(
|
||||
dlmatches[3] == '',
|
||||
'attachment; filename="' %.%
|
||||
gsub('(["\\\\])', '\\\\\\1', filename) %.% # yes, that many \'s
|
||||
'"',
|
||||
paste0(
|
||||
'attachment; filename="',
|
||||
gsub('(["\\\\])', '\\\\\\1', filename),
|
||||
'"'
|
||||
),
|
||||
'attachment'
|
||||
),
|
||||
'Cache-Control'='no-cache')))
|
||||
@@ -1890,33 +1985,18 @@ ShinySession <- R6Class(
|
||||
|
||||
return(httpResponse(404, 'text/html', '<h1>Not Found</h1>'))
|
||||
},
|
||||
saveFileUrl = function(name, data, contentType, extra=list()) {
|
||||
"Creates an entry in the file map for the data, and returns a URL pointing
|
||||
to the file."
|
||||
self$files$set(name, list(data=data, contentType=contentType))
|
||||
return(sprintf('session/%s/file/%s?w=%s&r=%s',
|
||||
URLencode(self$token, TRUE),
|
||||
URLencode(name, TRUE),
|
||||
workerId(),
|
||||
createUniqueId(8)))
|
||||
},
|
||||
# Send a file to the client
|
||||
fileUrl = function(name, file, contentType='application/octet-stream') {
|
||||
"Return a URL for a file to be sent to the client. If allowDataUriScheme
|
||||
is TRUE, then the file will be base64 encoded and embedded in the URL.
|
||||
Otherwise, a URL pointing to the file will be returned."
|
||||
"Return a URL for a file to be sent to the client. The file will be base64
|
||||
encoded and embedded in the URL."
|
||||
bytes <- file.info(file)$size
|
||||
if (is.na(bytes))
|
||||
return(NULL)
|
||||
|
||||
fileData <- readBin(file, 'raw', n=bytes)
|
||||
|
||||
if (isTRUE(private$.clientData$.values$get("allowDataUriScheme"))) {
|
||||
b64 <- rawToBase64(fileData)
|
||||
return(paste('data:', contentType, ';base64,', b64, sep=''))
|
||||
} else {
|
||||
return(self$saveFileUrl(name, fileData, contentType))
|
||||
}
|
||||
b64 <- rawToBase64(fileData)
|
||||
return(paste('data:', contentType, ';base64,', b64, sep=''))
|
||||
},
|
||||
registerDownload = function(name, filename, contentType, func) {
|
||||
|
||||
@@ -2051,9 +2131,11 @@ ShinySession <- R6Class(
|
||||
active = list(
|
||||
session = function() {
|
||||
shinyDeprecated(
|
||||
msg = paste("Attempted to access deprecated shinysession$session object.",
|
||||
"Please just access the shinysession object directly."),
|
||||
version = "0.11.1"
|
||||
"0.11.1", "shinysession$session",
|
||||
details = paste0(
|
||||
"Attempted to access deprecated shinysession$session object. ",
|
||||
"Please just access the shinysession object directly."
|
||||
)
|
||||
)
|
||||
self
|
||||
}
|
||||
@@ -2093,7 +2175,7 @@ ShinySession <- R6Class(
|
||||
if (getOption("shiny.allowoutputreads", FALSE)) {
|
||||
.subset2(x, 'impl')$getOutput(name)
|
||||
} else {
|
||||
stop("Reading from shinyoutput object is not allowed.")
|
||||
rlang::abort(paste0("Can't read output '", output, "'"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2102,12 +2184,12 @@ ShinySession <- R6Class(
|
||||
|
||||
#' @export
|
||||
`[.shinyoutput` <- function(values, name) {
|
||||
stop("Single-bracket indexing of shinyoutput object is not allowed.")
|
||||
rlang::abort("Can't index shinyoutput with `[`.")
|
||||
}
|
||||
|
||||
#' @export
|
||||
`[<-.shinyoutput` <- function(values, name, value) {
|
||||
stop("Single-bracket indexing of shinyoutput object is not allowed.")
|
||||
rlang::abort("Can't index shinyoutput with `[[`.")
|
||||
}
|
||||
|
||||
#' Set options for an output object.
|
||||
@@ -2459,3 +2541,19 @@ markdown <- function(mds, extensions = TRUE, .noWS = NULL, ...) {
|
||||
html <- rlang::exec(commonmark::markdown_html, glue::trim(mds), extensions = extensions, ...)
|
||||
htmltools::HTML(html, .noWS = .noWS)
|
||||
}
|
||||
|
||||
|
||||
# Check that an object is a ShinySession object, and give an informative error.
|
||||
# The default label is the caller function's name.
|
||||
validate_session_object <- function(session, label = as.character(sys.call(sys.parent())[[1]])) {
|
||||
if (missing(session) ||
|
||||
!inherits(session, c("ShinySession", "MockShinySession", "session_proxy")))
|
||||
{
|
||||
stop(call. = FALSE,
|
||||
sprintf(
|
||||
"`session` must be a 'ShinySession' object. Did you forget to pass `session` to `%s()`?",
|
||||
label
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +93,7 @@ shinyApp <- function(ui, server, onStart=NULL, options=list(),
|
||||
|
||||
# Store the appDir and bookmarking-related options, so that we can read them
|
||||
# from within the app.
|
||||
shinyOptions(appDir = getwd())
|
||||
appOptions <- consumeAppOptions()
|
||||
appOptions <- captureAppOptions()
|
||||
|
||||
structure(
|
||||
list(
|
||||
@@ -114,7 +113,10 @@ shinyApp <- function(ui, server, onStart=NULL, options=list(),
|
||||
#' @export
|
||||
shinyAppDir <- function(appDir, options=list()) {
|
||||
if (!utils::file_test('-d', appDir)) {
|
||||
stop("No Shiny application exists at the path \"", appDir, "\"")
|
||||
rlang::abort(
|
||||
paste0("No Shiny application exists at the path \"", appDir, "\""),
|
||||
class = "invalidShinyAppDir"
|
||||
)
|
||||
}
|
||||
|
||||
# In case it's a relative path, convert to absolute (so we're not adversely
|
||||
@@ -126,7 +128,10 @@ shinyAppDir <- function(appDir, options=list()) {
|
||||
} else if (file.exists.ci(appDir, "app.R")) {
|
||||
shinyAppDir_appR("app.R", appDir, options = options)
|
||||
} else {
|
||||
stop("App dir must contain either app.R or server.R.")
|
||||
rlang::abort(
|
||||
"App dir must contain either app.R or server.R.",
|
||||
class = "invalidShinyAppDir"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,6 +351,17 @@ loadSupport <- function(appDir=NULL, renv=new.env(parent=globalenv()), globalren
|
||||
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)){
|
||||
# Evaluate global.R, if it exists.
|
||||
globalPath <- file.path.ci(appDir, "global.R")
|
||||
@@ -408,7 +424,7 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
|
||||
if (!is.shiny.appobj(result))
|
||||
stop("app.R did not return a shiny.appobj object.")
|
||||
|
||||
unconsumeAppOptions(result$appOptions)
|
||||
applyCapturedAppOptions(result$appOptions)
|
||||
|
||||
return(result)
|
||||
}
|
||||
@@ -559,7 +575,7 @@ as.tags.shiny.appobj <- function(x, ...) {
|
||||
# jcheng 06/06/2014: Unfortunate copy/paste between this function and
|
||||
# knit_print.shiny.appobj, but I am trying to make the most conservative
|
||||
# change possible due to upcoming release.
|
||||
opts <- x$options %OR% list()
|
||||
opts <- x$options %||% list()
|
||||
width <- if (is.null(opts$width)) "100%" else opts$width
|
||||
height <- if (is.null(opts$height)) "400" else opts$height
|
||||
|
||||
@@ -579,84 +595,3 @@ deferredIFrame <- function(path, width, height) {
|
||||
class = "shiny-frame shiny-frame-deferred"
|
||||
)
|
||||
}
|
||||
|
||||
#' Knitr S3 methods
|
||||
#'
|
||||
#' These S3 methods are necessary to help Shiny applications and UI chunks embed
|
||||
#' themselves in knitr/rmarkdown documents.
|
||||
#'
|
||||
#' @name knitr_methods
|
||||
#' @param x Object to knit_print
|
||||
#' @param ... Additional knit_print arguments
|
||||
NULL
|
||||
|
||||
# If there's an R Markdown runtime option set but it isn't set to Shiny, then
|
||||
# return a warning indicating the runtime is inappropriate for this object.
|
||||
# Returns NULL in all other cases.
|
||||
shiny_rmd_warning <- function() {
|
||||
runtime <- knitr::opts_knit$get("rmarkdown.runtime")
|
||||
if (!is.null(runtime) && runtime != "shiny")
|
||||
# note that the RStudio IDE checks for this specific string to detect Shiny
|
||||
# applications in static document
|
||||
list(structure(
|
||||
"Shiny application in a static R Markdown document",
|
||||
class = "rmd_warning"))
|
||||
else
|
||||
NULL
|
||||
}
|
||||
|
||||
#' @rdname knitr_methods
|
||||
knit_print.shiny.appobj <- function(x, ...) {
|
||||
opts <- x$options %OR% list()
|
||||
width <- if (is.null(opts$width)) "100%" else opts$width
|
||||
height <- if (is.null(opts$height)) "400" else opts$height
|
||||
|
||||
runtime <- knitr::opts_knit$get("rmarkdown.runtime")
|
||||
if (!is.null(runtime) && runtime != "shiny") {
|
||||
# If not rendering to a Shiny document, create a box exactly the same
|
||||
# dimensions as the Shiny app would have had (so the document continues to
|
||||
# flow as it would have with the app), and display a diagnostic message
|
||||
width <- validateCssUnit(width)
|
||||
height <- validateCssUnit(height)
|
||||
output <- tags$div(
|
||||
style=paste("width:", width, "; height:", height, "; text-align: center;",
|
||||
"box-sizing: border-box;", "-moz-box-sizing: border-box;",
|
||||
"-webkit-box-sizing: border-box;"),
|
||||
class="muted well",
|
||||
"Shiny applications not supported in static R Markdown documents")
|
||||
}
|
||||
else {
|
||||
path <- addSubApp(x)
|
||||
output <- deferredIFrame(path, width, height)
|
||||
}
|
||||
|
||||
# If embedded Shiny apps ever have JS/CSS dependencies (like pym.js) we'll
|
||||
# need to grab those and put them in meta, like in knit_print.shiny.tag. But
|
||||
# for now it's not an issue, so just return the HTML and warning.
|
||||
|
||||
knitr::asis_output(htmlPreserve(format(output, indent=FALSE)),
|
||||
meta = shiny_rmd_warning(), cacheable = FALSE)
|
||||
}
|
||||
|
||||
# Let us use a nicer syntax in knitr chunks than literally
|
||||
# calling output$value <- renderFoo(...) and fooOutput().
|
||||
#' @rdname knitr_methods
|
||||
#' @param inline Whether the object is printed inline.
|
||||
knit_print.shiny.render.function <- function(x, ..., inline = FALSE) {
|
||||
x <- htmltools::as.tags(x, inline = inline)
|
||||
output <- knitr::knit_print(tagList(x))
|
||||
attr(output, "knit_cacheable") <- FALSE
|
||||
attr(output, "knit_meta") <- append(attr(output, "knit_meta"),
|
||||
shiny_rmd_warning())
|
||||
output
|
||||
}
|
||||
|
||||
# Lets us drop reactive expressions directly into a knitr chunk and have the
|
||||
# value printed out! Nice for teaching if nothing else.
|
||||
#' @rdname knitr_methods
|
||||
knit_print.reactive <- function(x, ..., inline = FALSE) {
|
||||
renderFunc <- if (inline) renderText else renderPrint
|
||||
knitr::knit_print(renderFunc({
|
||||
x()
|
||||
}), inline = inline)
|
||||
}
|
||||
87
R/shinyui.R
87
R/shinyui.R
@@ -25,6 +25,8 @@ withMathJax <- function(...) {
|
||||
}
|
||||
|
||||
renderPage <- function(ui, showcase=0, testMode=FALSE) {
|
||||
lang <- getLang(ui)
|
||||
|
||||
# If the ui is a NOT complete document (created by htmlTemplate()), then do some
|
||||
# preprocessing and make sure it's a complete document.
|
||||
if (!inherits(ui, "html_document")) {
|
||||
@@ -38,7 +40,10 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
|
||||
# Put the body into the default template
|
||||
ui <- htmlTemplate(
|
||||
system.file("template", "default.html", package = "shiny"),
|
||||
body = ui
|
||||
lang = lang,
|
||||
body = ui,
|
||||
# this template is a complete HTML document
|
||||
document_ = TRUE
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,7 +51,7 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
|
||||
version <- getOption("shiny.jquery.version", 3)
|
||||
if (version == 3) {
|
||||
return(htmlDependency(
|
||||
"jquery", "3.5.1",
|
||||
"jquery", version_jquery,
|
||||
c(href = "shared"),
|
||||
script = "jquery.min.js"
|
||||
))
|
||||
@@ -61,28 +66,73 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
|
||||
stop("Unsupported version of jQuery: ", version)
|
||||
}
|
||||
|
||||
shiny_deps <- list(
|
||||
htmlDependency("json2", "2014.02.04", c(href="shared"), script = "json2-min.js"),
|
||||
jquery(),
|
||||
htmlDependency("shiny", utils::packageVersion("shiny"), c(href="shared"),
|
||||
script = if (getOption("shiny.minified", TRUE)) "shiny.min.js" else "shiny.js",
|
||||
stylesheet = "shiny.css")
|
||||
shiny_deps <- c(
|
||||
list(jquery()),
|
||||
shinyDependencies()
|
||||
)
|
||||
|
||||
if (testMode) {
|
||||
# Add code injection listener if in test mode
|
||||
shiny_deps[[length(shiny_deps) + 1]] <-
|
||||
htmlDependency("shiny-testmode", utils::packageVersion("shiny"),
|
||||
c(href="shared"), script = "shiny-testmode.js")
|
||||
htmlDependency("shiny-testmode", shinyPackageVersion(),
|
||||
c(href="shared"), script = "shiny-testmode.js")
|
||||
}
|
||||
|
||||
html <- renderDocument(ui, shiny_deps, processDep = createWebDependency)
|
||||
enc2utf8(paste(collapse = "\n", html))
|
||||
}
|
||||
|
||||
shinyDependencies <- function() {
|
||||
list(
|
||||
bslib::bs_dependency_defer(shinyDependencyCSS),
|
||||
htmlDependency(
|
||||
name = "shiny-javascript",
|
||||
version = shinyPackageVersion(),
|
||||
src = c(href = "shared"),
|
||||
script =
|
||||
if (isTRUE(
|
||||
get_devmode_option(
|
||||
"shiny.minified",
|
||||
TRUE
|
||||
)
|
||||
))
|
||||
"shiny.min.js"
|
||||
else
|
||||
"shiny.js"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
shinyDependencyCSS <- function(theme) {
|
||||
version <- shinyPackageVersion()
|
||||
|
||||
if (!is_bs_theme(theme)) {
|
||||
return(htmlDependency(
|
||||
name = "shiny-css",
|
||||
version = version,
|
||||
src = c(href = "shared"),
|
||||
stylesheet = "shiny.min.css"
|
||||
))
|
||||
}
|
||||
|
||||
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(
|
||||
input = scss_files,
|
||||
theme = theme,
|
||||
name = "shiny-sass",
|
||||
version = version,
|
||||
cache_key_extra = version
|
||||
)
|
||||
}
|
||||
|
||||
#' Create a Shiny UI handler
|
||||
#'
|
||||
#' Historically this function was used in ui.R files to register a user
|
||||
#' @description \lifecycle{superseded}
|
||||
#'
|
||||
#' @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
|
||||
#' ensure that the last expression to be returned from ui.R is a user interface.
|
||||
#' This function is kept for backwards compatibility with older applications. It
|
||||
@@ -93,6 +143,17 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
|
||||
#' @keywords internal
|
||||
#' @export
|
||||
shinyUI <- function(ui) {
|
||||
if (in_devmode()) {
|
||||
shinyDeprecated(
|
||||
"0.10.0", "shinyUI()",
|
||||
details = paste0(
|
||||
"When removing `shinyUI()`, ",
|
||||
"ensure that the last expression returned from ui.R is a user interface ",
|
||||
"normally supplied to `shinyUI(ui)`."
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
.globals$ui <- list(ui)
|
||||
ui
|
||||
}
|
||||
@@ -103,7 +164,7 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
|
||||
|
||||
allowed_methods <- "GET"
|
||||
if (is.function(ui)) {
|
||||
allowed_methods <- attr(ui, "http_methods_supported", exact = TRUE) %OR% allowed_methods
|
||||
allowed_methods <- attr(ui, "http_methods_supported", exact = TRUE) %||% allowed_methods
|
||||
}
|
||||
|
||||
function(req) {
|
||||
@@ -120,7 +181,7 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
|
||||
showcaseMode <- mode
|
||||
}
|
||||
|
||||
testMode <- .globals$testMode %OR% FALSE
|
||||
testMode <- getShinyOption("testmode", default = FALSE)
|
||||
|
||||
# Create a restore context using query string
|
||||
bookmarkStore <- getShinyOption("bookmarkStore", default = "disable")
|
||||
|
||||
@@ -1,34 +1,105 @@
|
||||
utils::globalVariables('func')
|
||||
utils::globalVariables('func', add = TRUE)
|
||||
|
||||
#' Mark a function as a render function
|
||||
#'
|
||||
#' 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 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 widgets out of just the render function.
|
||||
#' 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
|
||||
#' 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
|
||||
#' widgets out of just the render function.
|
||||
#'
|
||||
#' @param uiFunc A function that renders Shiny UI. Must take a single argument:
|
||||
#' an output ID.
|
||||
#' @param renderFunc A function that is suitable for assigning to a Shiny output
|
||||
#' slot.
|
||||
#' @param outputArgs A list of arguments to pass to the `uiFunc`. Render
|
||||
#' functions should include `outputArgs = list()` in their own parameter
|
||||
#' list, and pass through the value to `markRenderFunction`, to allow
|
||||
#' app authors to customize outputs. (Currently, this is only supported for
|
||||
#' dynamically generated UIs, such as those created by Shiny code snippets
|
||||
#' embedded in R Markdown documents).
|
||||
#' functions should include `outputArgs = list()` in their own parameter list,
|
||||
#' and pass through the value to `markRenderFunction`, to allow app authors to
|
||||
#' customize outputs. (Currently, this is only supported for dynamically
|
||||
#' generated UIs, such as those created by Shiny code snippets embedded in R
|
||||
#' Markdown documents).
|
||||
#' @param cacheHint One of `"auto"`, `FALSE`, or some other information to
|
||||
#' identify this instance for caching using [bindCache()]. If `"auto"`, it
|
||||
#' will try to automatically infer caching information. If `FALSE`, do not
|
||||
#' allow caching for the object. Some render functions (such as [renderPlot])
|
||||
#' contain internal state that makes them unsuitable for caching.
|
||||
#' @param cacheWriteHook Used if the render function is passed to `bindCache()`.
|
||||
#' This is an optional callback function to invoke before saving the value
|
||||
#' from the render function to the cache. This function must accept one
|
||||
#' argument, the value returned from `renderFunc`, and should return the value
|
||||
#' to store in the cache.
|
||||
#' @param cacheReadHook Used if the render function is passed to `bindCache()`.
|
||||
#' This is an optional callback function to invoke after reading a value from
|
||||
#' the cache (if there is a cache hit). The function will be passed one
|
||||
#' argument, the value retrieved from the cache. This can be useful when some
|
||||
#' side effect needs to occur for a render function to behave correctly. For
|
||||
#' example, some render functions call [createWebDependency()] so that Shiny
|
||||
#' is able to serve JS and CSS resources.
|
||||
#' @return The `renderFunc` function, with annotations.
|
||||
#'
|
||||
#' @seealso [createRenderFunction()], [quoToFunction()]
|
||||
#' @export
|
||||
markRenderFunction <- function(uiFunc, renderFunc, outputArgs = list()) {
|
||||
markRenderFunction <- function(
|
||||
uiFunc,
|
||||
renderFunc,
|
||||
outputArgs = list(),
|
||||
cacheHint = "auto",
|
||||
cacheWriteHook = NULL,
|
||||
cacheReadHook = NULL
|
||||
) {
|
||||
force(renderFunc)
|
||||
|
||||
# a mutable object that keeps track of whether `useRenderFunction` has been
|
||||
# executed (this usually only happens when rendering Shiny code snippets in
|
||||
# an interactive R Markdown document); its initial value is FALSE
|
||||
hasExecuted <- Mutable$new()
|
||||
hasExecuted$set(FALSE)
|
||||
|
||||
origRenderFunc <- renderFunc
|
||||
renderFunc <- function(...) {
|
||||
if (is.null(uiFunc)) {
|
||||
uiFunc <- function(id) {
|
||||
pre(
|
||||
"No UI/output function provided for render function. ",
|
||||
"Please see ?shiny::markRenderFunction and ?shiny::createRenderFunction."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (identical(cacheHint, "auto")) {
|
||||
origUserFunc <- attr(renderFunc, "wrappedFunc", exact = TRUE)
|
||||
# The result could be NULL, but don't warn now because it'll only affect
|
||||
# users if they try to use caching. We'll warn when someone calls
|
||||
# bindCache() on this object.
|
||||
if (is.null(origUserFunc)) {
|
||||
cacheHint <- NULL
|
||||
} else {
|
||||
# Add in the wrapper render function and they output function, because
|
||||
# they can be useful for distinguishing two renderX functions that receive
|
||||
# the same user expression but do different things with them (like
|
||||
# renderText and renderPrint).
|
||||
cacheHint <- list(
|
||||
origUserFunc = origUserFunc,
|
||||
renderFunc = renderFunc,
|
||||
outputFunc = uiFunc
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!is.null(cacheHint) && !is_false(cacheHint)) {
|
||||
if (!is.list(cacheHint)) {
|
||||
cacheHint <- list(cacheHint)
|
||||
}
|
||||
# For functions, remove the env and source refs because they can cause
|
||||
# spurious differences.
|
||||
# For expressions, remove source refs.
|
||||
# For everything else, do nothing.
|
||||
cacheHint <- lapply(cacheHint, function(x) {
|
||||
if (is.function(x)) formalsAndBody(x)
|
||||
else if (is.language(x)) zap_srcref(x)
|
||||
else x
|
||||
})
|
||||
}
|
||||
|
||||
wrappedRenderFunc <- function(...) {
|
||||
# if the user provided something through `outputArgs` BUT the
|
||||
# `useRenderFunction` was not executed, then outputArgs will be ignored,
|
||||
# so throw a warning to let user know the correct usage
|
||||
@@ -41,15 +112,20 @@ markRenderFunction <- function(uiFunc, renderFunc, outputArgs = list()) {
|
||||
# stop warning from happening again for the same object
|
||||
hasExecuted$set(TRUE)
|
||||
}
|
||||
if (is.null(formals(origRenderFunc))) origRenderFunc()
|
||||
else origRenderFunc(...)
|
||||
if (is.null(formals(renderFunc))) renderFunc()
|
||||
else renderFunc(...)
|
||||
}
|
||||
|
||||
structure(renderFunc,
|
||||
class = c("shiny.render.function", "function"),
|
||||
outputFunc = uiFunc,
|
||||
outputArgs = outputArgs,
|
||||
hasExecuted = hasExecuted)
|
||||
structure(
|
||||
wrappedRenderFunc,
|
||||
class = c("shiny.render.function", "function"),
|
||||
outputFunc = uiFunc,
|
||||
outputArgs = outputArgs,
|
||||
hasExecuted = hasExecuted,
|
||||
cacheHint = cacheHint,
|
||||
cacheWriteHook = cacheWriteHook,
|
||||
cacheReadHook = cacheReadHook
|
||||
)
|
||||
}
|
||||
|
||||
#' @export
|
||||
@@ -59,6 +135,9 @@ print.shiny.render.function <- function(x, ...) {
|
||||
|
||||
#' Implement render functions
|
||||
#'
|
||||
#' This function is a wrapper for [markRenderFunction()] which provides support
|
||||
#' for async computation via promises.
|
||||
#'
|
||||
#' @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
|
||||
#' mode.
|
||||
@@ -70,34 +149,63 @@ print.shiny.render.function <- function(x, ...) {
|
||||
#' @param outputFunc The UI function that is used (or most commonly used) with
|
||||
#' this render function. This can be used in R Markdown documents to create
|
||||
#' complete output widgets out of just the render function.
|
||||
#' @param outputArgs A list of arguments to pass to the `outputFunc`.
|
||||
#' Render functions should include `outputArgs = list()` in their own
|
||||
#' parameter list, and pass through the value as this argument, to allow app
|
||||
#' authors to customize outputs. (Currently, this is only supported for
|
||||
#' dynamically generated UIs, such as those created by Shiny code snippets
|
||||
#' embedded in R Markdown documents).
|
||||
#' @inheritParams markRenderFunction
|
||||
#' @return An annotated render function, ready to be assigned to an
|
||||
#' `output` slot.
|
||||
#'
|
||||
#' @seealso [quoToFunction()], [markRenderFunction()].
|
||||
#'
|
||||
#' @examples
|
||||
#' # A very simple render function
|
||||
#' renderTriple <- function(x) {
|
||||
#' x <- substitute(x)
|
||||
#' if (!rlang::is_quosure(x)) {
|
||||
#' x <- rlang::new_quosure(x, env = parent.frame())
|
||||
#' }
|
||||
#' func <- quoToFunction(x, "renderTriple")
|
||||
#'
|
||||
#' createRenderFunction(
|
||||
#' func,
|
||||
#' transform = function(value, session, name, ...) {
|
||||
#' paste(rep(value, 3), collapse=", ")
|
||||
#' },
|
||||
#' outputFunc = textOutput
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' # Test render function from the console
|
||||
#' a <- 1
|
||||
#' r <- renderTriple({ a + 1 })
|
||||
#' a <- 2
|
||||
#' r()
|
||||
#' @export
|
||||
createRenderFunction <- function(
|
||||
func, transform = function(value, session, name, ...) value,
|
||||
outputFunc = NULL, outputArgs = NULL
|
||||
func,
|
||||
transform = function(value, session, name, ...) value,
|
||||
outputFunc = NULL,
|
||||
outputArgs = NULL,
|
||||
cacheHint = "auto",
|
||||
cacheWriteHook = NULL,
|
||||
cacheReadHook = NULL
|
||||
) {
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
hybrid_chain(
|
||||
func(),
|
||||
function(value, .visible) {
|
||||
transform(setVisible(value, .visible), shinysession, name, ...)
|
||||
function(value) {
|
||||
transform(value, shinysession, name, ...)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!is.null(outputFunc))
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
|
||||
else
|
||||
renderFunc
|
||||
# Hoist func's wrappedFunc attribute into renderFunc, so that when we pass
|
||||
# renderFunc on to markRenderFunction, it is able to find the original user
|
||||
# function.
|
||||
if (identical(cacheHint, "auto")) {
|
||||
attr(renderFunc, "wrappedFunc") <- attr(func, "wrappedFunc", exact = TRUE)
|
||||
}
|
||||
|
||||
markRenderFunction(outputFunc, renderFunc, outputArgs, cacheHint,
|
||||
cacheWriteHook, cacheReadHook)
|
||||
}
|
||||
|
||||
useRenderFunction <- function(renderFunc, inline = FALSE) {
|
||||
@@ -140,6 +248,22 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
|
||||
useRenderFunction(x, inline = inline)
|
||||
}
|
||||
|
||||
# Get relevant attributes from a render function object.
|
||||
renderFunctionAttributes <- function(x) {
|
||||
attrs <- c("outputFunc", "outputArgs", "hasExecuted", "cacheHint")
|
||||
names(attrs) <- attrs
|
||||
lapply(attrs, function(name) attr(x, name, exact = TRUE))
|
||||
}
|
||||
|
||||
# Add a named list of attributes to an object
|
||||
addAttributes <- function(x, attrs) {
|
||||
nms <- names(attrs)
|
||||
for (i in seq_along(attrs)) {
|
||||
attr(x, nms[i]) <- attrs[[i]]
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
|
||||
#' Mark a render function with attributes that will be used by the output
|
||||
#'
|
||||
@@ -274,8 +398,10 @@ markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
deleteFile, outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
deleteFile, outputArgs=list())
|
||||
{
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
func <- quoToFunction(expr, "renderImage")
|
||||
|
||||
# missing() must be used directly within the function with the given arg
|
||||
if (missing(deleteFile)) {
|
||||
@@ -324,7 +450,7 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
}
|
||||
|
||||
# If contentType not specified, autodetect based on extension
|
||||
contentType <- imageinfo$contentType %OR% getContentType(imageinfo$src)
|
||||
contentType <- imageinfo$contentType %||% getContentType(imageinfo$src)
|
||||
|
||||
# Extra values are everything in imageinfo except 'src' and 'contentType'
|
||||
extra_attr <- imageinfo[!names(imageinfo) %in% c('src', 'contentType')]
|
||||
@@ -333,7 +459,10 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
c(src = session$fileUrl(name, file=imageinfo$src, contentType=contentType),
|
||||
extra_attr)
|
||||
},
|
||||
imageOutput, outputArgs)
|
||||
imageOutput,
|
||||
outputArgs,
|
||||
cacheHint = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
# TODO: If we ever take a dependency on fs, it'd be great to replace this with
|
||||
@@ -405,8 +534,10 @@ isTemp <- function(path, tempDir = tempdir(), mustExist) {
|
||||
#' @example res/text-example.R
|
||||
#' @export
|
||||
renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
width = getOption('width'), outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
width = getOption('width'), outputArgs=list())
|
||||
{
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
func <- quoToFunction(expr, "renderPrint")
|
||||
|
||||
# Set a promise domain that sets the console width
|
||||
# and captures output
|
||||
@@ -419,12 +550,12 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
{
|
||||
promises::with_promise_domain(domain, func())
|
||||
},
|
||||
function(value, .visible) {
|
||||
if (.visible) {
|
||||
cat(file = domain$conn, paste(utils::capture.output(value, append = TRUE), collapse = "\n"))
|
||||
function(value) {
|
||||
res <- withVisible(value)
|
||||
if (res$visible) {
|
||||
cat(file = domain$conn, paste(utils::capture.output(res$value, append = TRUE), collapse = "\n"))
|
||||
}
|
||||
res <- paste(readLines(domain$conn, warn = FALSE), collapse = "\n")
|
||||
res
|
||||
paste(readLines(domain$conn, warn = FALSE), collapse = "\n")
|
||||
},
|
||||
finally = function() {
|
||||
close(domain$conn)
|
||||
@@ -432,7 +563,15 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
)
|
||||
}
|
||||
|
||||
markRenderFunction(verbatimTextOutput, renderFunc, outputArgs = outputArgs)
|
||||
markRenderFunction(
|
||||
verbatimTextOutput,
|
||||
renderFunc,
|
||||
outputArgs,
|
||||
cacheHint = list(
|
||||
label = "renderPrint",
|
||||
origUserExpr = get_expr(expr)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
createRenderPrintPromiseDomain <- function(width) {
|
||||
@@ -482,14 +621,17 @@ createRenderPrintPromiseDomain <- function(width) {
|
||||
#' @rdname renderPrint
|
||||
renderText <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
outputArgs=list(), sep=" ") {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
func <- quoToFunction(expr, "renderText")
|
||||
|
||||
createRenderFunction(
|
||||
func,
|
||||
function(value, session, name, ...) {
|
||||
paste(utils::capture.output(cat(value, sep=sep)), collapse="\n")
|
||||
},
|
||||
textOutput, outputArgs
|
||||
textOutput,
|
||||
outputArgs
|
||||
)
|
||||
}
|
||||
|
||||
@@ -530,9 +672,11 @@ renderText <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
renderUI <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
outputArgs = list())
|
||||
{
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
func <- quoToFunction(expr, "renderUI")
|
||||
|
||||
createRenderFunction(
|
||||
func,
|
||||
@@ -542,7 +686,8 @@ renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
|
||||
processDeps(result, shinysession)
|
||||
},
|
||||
uiOutput, outputArgs
|
||||
uiOutput,
|
||||
outputArgs
|
||||
)
|
||||
}
|
||||
|
||||
@@ -565,7 +710,7 @@ renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
|
||||
#' that file path. (Reactive values and functions may be used from this
|
||||
#' function.)
|
||||
#' @param contentType A string of the download's
|
||||
#' [content type](http://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` or
|
||||
#' `NA`, the content type will be guessed based on the filename
|
||||
#' extension, or `application/octet-stream` if the extension is unknown.
|
||||
@@ -603,32 +748,41 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
|
||||
shinysession$registerDownload(name, filename, contentType, content)
|
||||
}
|
||||
snapshotExclude(
|
||||
markRenderFunction(downloadButton, renderFunc, outputArgs = outputArgs)
|
||||
markRenderFunction(downloadButton, renderFunc, outputArgs, cacheHint = FALSE)
|
||||
)
|
||||
}
|
||||
|
||||
#' Table output with the JavaScript library DataTables
|
||||
#' Table output with the JavaScript DataTables library
|
||||
#'
|
||||
#' @description
|
||||
#' Makes a reactive version of the given function that returns a data frame (or
|
||||
#' matrix), which will be rendered with the DataTables library. Paging,
|
||||
#' searching, filtering, and sorting can be done on the R side using Shiny as
|
||||
#' the server infrastructure.
|
||||
#' 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 only provides the server-side version of DataTables (using R
|
||||
#' to process the data object on the server side). There is a separate
|
||||
#' [DT](https://github.com/rstudio/DT) that allows you to create both
|
||||
#' server-side and client-side DataTables, and supports additional features.
|
||||
#' Learn more at <https://rstudio.github.io/DT/shiny.html>.
|
||||
#'
|
||||
#' For the `options` argument, the character elements that have the class
|
||||
#' `"AsIs"` (usually returned from [base::I()]) will be evaluated in
|
||||
#' JavaScript. This is useful when the type of the option value is not supported
|
||||
#' in JSON, e.g., a JavaScript function, which can be obtained by evaluating a
|
||||
#' character string. Note this only applies to the root-level elements of the
|
||||
#' options list, and the `I()` notation does not work for lower-level
|
||||
#' elements in the list.
|
||||
#' @param expr An expression that returns a data frame or a matrix.
|
||||
#' @inheritParams renderTable
|
||||
#' @param options A list of initialization options to be passed to DataTables,
|
||||
#' or a function to return such a list.
|
||||
#' or a function to return such a list. You can find a complete list of
|
||||
#' options at <https://datatables.net/reference/option/>.
|
||||
#'
|
||||
#' Any top-level strings with class `"AsIs"` (as created by [I()]) will be
|
||||
#' evaluated in JavaScript. This is useful when the type of the option value
|
||||
#' is not supported in JSON, e.g., a JavaScript function, which can be
|
||||
#' obtained by evaluating a character string. This only applies to the
|
||||
#' root-level elements of options list, and does not worked for lower-level
|
||||
#' elements in the list.
|
||||
#' @param searchDelay The delay for searching, in milliseconds (to avoid too
|
||||
#' frequent search requests).
|
||||
#' @param callback A JavaScript function to be applied to the DataTable object.
|
||||
#' This is useful for DataTables plug-ins, which often require the DataTable
|
||||
#' instance to be available (<http://datatables.net/extensions/>).
|
||||
#' instance to be available.
|
||||
#' @param escape Whether to escape HTML entities in the table: `TRUE` means
|
||||
#' to escape the whole table, and `FALSE` means not to escape it.
|
||||
#' Alternatively, you can specify numeric column indices or column names to
|
||||
@@ -636,17 +790,8 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
|
||||
#' `c(1, 3, 4)`, or `c(-1, -3)` (all columns except the first and
|
||||
#' third), or `c('Species', 'Sepal.Length')`.
|
||||
#' @param outputArgs A list of arguments to be passed through to the implicit
|
||||
#' call to [dataTableOutput()] when `renderDataTable` is used
|
||||
#' call to `dataTableOutput()` when `renderDataTable()` is used
|
||||
#' in an interactive R Markdown document.
|
||||
#'
|
||||
#' @references <http://datatables.net>
|
||||
#' @note This function only provides the server-side version of DataTables
|
||||
#' (using R to process the data object on the server side). There is a
|
||||
#' separate package \pkg{DT} (<https://github.com/rstudio/DT>) that allows
|
||||
#' you to create both server-side and client-side DataTables, and supports
|
||||
#' additional DataTables features. Consider using `DT::renderDataTable()`
|
||||
#' and `DT::dataTableOutput()` (see
|
||||
#' <http://rstudio.github.io/DT/shiny.html> for more information).
|
||||
#' @export
|
||||
#' @inheritParams renderPlot
|
||||
#' @examples
|
||||
@@ -674,8 +819,18 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
|
||||
renderDataTable <- function(expr, options = NULL, searchDelay = 500,
|
||||
callback = 'function(oTable) {}', escape = TRUE,
|
||||
env = parent.frame(), quoted = FALSE,
|
||||
outputArgs=list()) {
|
||||
installExprFunction(expr, "func", env, quoted)
|
||||
outputArgs=list())
|
||||
{
|
||||
|
||||
if (in_devmode()) {
|
||||
shinyDeprecated(
|
||||
"0.11.1", "shiny::renderDataTable()", "DT::renderDataTable()",
|
||||
details = "See <https://rstudio.github.io/DT/shiny.html> for more information"
|
||||
)
|
||||
}
|
||||
|
||||
expr <- get_quosure(expr, env, quoted)
|
||||
func <- quoToFunction(expr, "renderDataTable")
|
||||
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
if (is.function(options)) options <- options()
|
||||
@@ -709,7 +864,8 @@ renderDataTable <- function(expr, options = NULL, searchDelay = 500,
|
||||
)
|
||||
}
|
||||
|
||||
renderFunc <- markRenderFunction(dataTableOutput, renderFunc, outputArgs = outputArgs)
|
||||
renderFunc <- markRenderFunction(dataTableOutput, renderFunc, outputArgs,
|
||||
cacheHint = FALSE)
|
||||
|
||||
renderFunc <- snapshotPreprocessOutput(renderFunc, function(value) {
|
||||
# Remove the action field so that it's not saved in test snapshots. It
|
||||
@@ -766,6 +922,9 @@ checkDT9 <- function(options) {
|
||||
# Deprecated functions ------------------------------------------------------
|
||||
|
||||
#' Deprecated reactive functions
|
||||
#'
|
||||
#' @description \lifecycle{superseded}
|
||||
#'
|
||||
#' @name deprecatedReactives
|
||||
#' @keywords internal
|
||||
NULL
|
||||
@@ -780,7 +939,7 @@ NULL
|
||||
#' @rdname deprecatedReactives
|
||||
#' @export
|
||||
reactivePlot <- function(func, width='auto', height='auto', ...) {
|
||||
shinyDeprecated(new="renderPlot")
|
||||
shinyDeprecated("0.4.0", "reactivePlot()", "renderPlot()")
|
||||
renderPlot({ func() }, width=width, height=height, ...)
|
||||
}
|
||||
|
||||
@@ -790,7 +949,7 @@ reactivePlot <- function(func, width='auto', height='auto', ...) {
|
||||
#' @rdname deprecatedReactives
|
||||
#' @export
|
||||
reactiveTable <- function(func, ...) {
|
||||
shinyDeprecated(new="renderTable")
|
||||
shinyDeprecated("0.4.0", "reactiveTable()", "renderTable()")
|
||||
renderTable({ func() })
|
||||
}
|
||||
|
||||
@@ -800,7 +959,7 @@ reactiveTable <- function(func, ...) {
|
||||
#' @rdname deprecatedReactives
|
||||
#' @export
|
||||
reactivePrint <- function(func) {
|
||||
shinyDeprecated(new="renderPrint")
|
||||
shinyDeprecated("0.4.0", "reactivePrint()", "renderPrint()")
|
||||
renderPrint({ func() })
|
||||
}
|
||||
|
||||
@@ -810,7 +969,7 @@ reactivePrint <- function(func) {
|
||||
#' @rdname deprecatedReactives
|
||||
#' @export
|
||||
reactiveUI <- function(func) {
|
||||
shinyDeprecated(new="renderUI")
|
||||
shinyDeprecated("0.4.0", "reactiveUI()", "renderUI()")
|
||||
renderUI({ func() })
|
||||
}
|
||||
|
||||
@@ -820,6 +979,6 @@ reactiveUI <- function(func) {
|
||||
#' @rdname deprecatedReactives
|
||||
#' @export
|
||||
reactiveText <- function(func) {
|
||||
shinyDeprecated(new="renderText")
|
||||
shinyDeprecated("0.4.0", "reactiveText()", "renderText()")
|
||||
renderText({ func() })
|
||||
}
|
||||
|
||||
@@ -220,4 +220,3 @@ showcaseUI <- function(ui) {
|
||||
showcaseBody(ui)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
70
R/stack.R
70
R/stack.R
@@ -1,70 +0,0 @@
|
||||
# A Stack object backed by a list. The backing list will grow or shrink as
|
||||
# the stack changes in size.
|
||||
Stack <- R6Class(
|
||||
'Stack',
|
||||
portable = FALSE,
|
||||
class = FALSE,
|
||||
public = list(
|
||||
|
||||
initialize = function(init = 20L) {
|
||||
# init is the initial size of the list. It is also used as the minimum
|
||||
# size of the list as it shrinks.
|
||||
private$stack <- vector("list", init)
|
||||
private$init <- init
|
||||
},
|
||||
|
||||
push = function(..., .list = NULL) {
|
||||
args <- c(list(...), .list)
|
||||
new_size <- count + length(args)
|
||||
|
||||
# Grow if needed; double in size
|
||||
while (new_size > length(stack)) {
|
||||
stack[length(stack) * 2] <<- list(NULL)
|
||||
}
|
||||
stack[count + seq_along(args)] <<- args
|
||||
count <<- new_size
|
||||
|
||||
invisible(self)
|
||||
},
|
||||
|
||||
pop = function() {
|
||||
if (count == 0L)
|
||||
return(NULL)
|
||||
|
||||
value <- stack[[count]]
|
||||
stack[count] <<- list(NULL)
|
||||
count <<- count - 1L
|
||||
|
||||
# Shrink list if < 1/4 of the list is used, down to a minimum size of `init`
|
||||
len <- length(stack)
|
||||
if (len > init && count < len/4) {
|
||||
new_len <- max(init, ceiling(len/2))
|
||||
stack <<- stack[seq_len(new_len)]
|
||||
}
|
||||
|
||||
value
|
||||
},
|
||||
|
||||
peek = function() {
|
||||
if (count == 0L)
|
||||
return(NULL)
|
||||
stack[[count]]
|
||||
},
|
||||
|
||||
size = function() {
|
||||
count
|
||||
},
|
||||
|
||||
# Return the entire stack as a list, where the first item in the list is the
|
||||
# oldest item in the stack, and the last item is the most recently added.
|
||||
as_list = function() {
|
||||
stack[seq_len(count)]
|
||||
}
|
||||
),
|
||||
|
||||
private = list(
|
||||
stack = NULL, # A list that holds the items
|
||||
count = 0L, # Current number of items in the stack
|
||||
init = 20L # Initial and minimum size of the stack
|
||||
)
|
||||
)
|
||||
151
R/test-server.R
151
R/test-server.R
@@ -1,19 +1,14 @@
|
||||
#' @noRd
|
||||
isModuleServer <- function(x) {
|
||||
is.function(x) && names(formals(x))[1] == "id"
|
||||
}
|
||||
|
||||
#' Reactive testing for Shiny server functions and modules
|
||||
#'
|
||||
#' A way to test the reactive interactions in Shiny applications. Reactive
|
||||
#' interactions are defined in the server function of applications and in
|
||||
#' modules.
|
||||
#' @param app The path to an application or module to test. In addition to
|
||||
#' paths, applications may be represented by any object suitable for coercion
|
||||
#' to an `appObj` by [`as.shiny.appobj`]. Application server functions must
|
||||
#' include a `session` argument in order to be tested. If `app` is `NULL` or
|
||||
#' not supplied, the nearest enclosing directory that is a Shiny app, starting
|
||||
#' with the current directory, is used.
|
||||
#' @param app A server function (i.e. a function with `input`, `output`,
|
||||
#' and `session`), or a module function (i.e. a function with first
|
||||
#' argument `id` that calls [moduleServer()].
|
||||
#'
|
||||
#' You can also provide an app, a path an app, or anything that
|
||||
#' [`as.shiny.appobj()`] can handle.
|
||||
#' @param expr Test code containing expectations. The objects from inside the
|
||||
#' server function environment will be made available in the environment of
|
||||
#' the test expression (this is done using a data mask with
|
||||
@@ -30,7 +25,19 @@ isModuleServer <- function(x) {
|
||||
#' @include mock-session.R
|
||||
#' @rdname testServer
|
||||
#' @examples
|
||||
#' server <- function(id, multiplier = 2, prefix = "I am ") {
|
||||
#' # Testing a server function ----------------------------------------------
|
||||
#' server <- function(input, output, session) {
|
||||
#' x <- reactive(input$a * input$b)
|
||||
#' }
|
||||
#'
|
||||
#' testServer(server, {
|
||||
#' session$setInputs(a = 2, b = 3)
|
||||
#' stopifnot(x() == 6)
|
||||
#' })
|
||||
#'
|
||||
#'
|
||||
#' # Testing a module --------------------------------------------------------
|
||||
#' myModuleServer <- function(id, multiplier = 2, prefix = "I am ") {
|
||||
#' moduleServer(id, function(input, output, session) {
|
||||
#' myreactive <- reactive({
|
||||
#' input$x * multiplier
|
||||
@@ -41,7 +48,7 @@ isModuleServer <- function(x) {
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' testServer(server, args = list(multiplier = 2), {
|
||||
#' testServer(myModuleServer, args = list(multiplier = 2), {
|
||||
#' session$setInputs(x = 1)
|
||||
#' # You're also free to use third-party
|
||||
#' # testing packages like testthat:
|
||||
@@ -56,74 +63,94 @@ isModuleServer <- function(x) {
|
||||
#' })
|
||||
#' @export
|
||||
testServer <- function(app = NULL, expr, args = list(), session = MockShinySession$new()) {
|
||||
|
||||
require(shiny)
|
||||
|
||||
if (!is.null(getDefaultReactiveDomain()))
|
||||
stop("testServer() is for use only within tests and may not indirectly call itself.")
|
||||
|
||||
on.exit(if (!session$isClosed()) session$close(), add = TRUE)
|
||||
quosure <- rlang::enquo(expr)
|
||||
|
||||
on.exit(if (!session$isClosed()) session$close())
|
||||
|
||||
withMockContext <- function(expr) {
|
||||
isolate(
|
||||
withReactiveDomain(session, {
|
||||
withr::with_options(list(`shiny.allowoutputreads` = TRUE), {
|
||||
withLocalOptions({
|
||||
# Sets a cache for renderCachedPlot() with cache = "app" to use.
|
||||
shinyOptions("cache" = session$appcache)
|
||||
expr
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (isModuleServer(app)) {
|
||||
if (!("id" %in% names(args)))
|
||||
args[["id"]] <- session$genId()
|
||||
# app is presumed to be a module, and modules may take additional arguments,
|
||||
# so splice in any args.
|
||||
withMockContext(rlang::exec(app, !!!args))
|
||||
withMockContext(session, rlang::exec(app, !!!args))
|
||||
|
||||
# If app is a module, then we must use both the module function's immediate
|
||||
# environment and also its enclosing environment to construct the mask.
|
||||
parent_clone <- rlang::env_clone(parent.env(session$env))
|
||||
clone <- rlang::env_clone(session$env, parent_clone)
|
||||
mask <- rlang::new_data_mask(clone, parent_clone)
|
||||
withMockContext(rlang::eval_tidy(quosure, mask, rlang::caller_env()))
|
||||
} else {
|
||||
if (is.null(app)) {
|
||||
app <- findEnclosingApp(".")
|
||||
}
|
||||
|
||||
appobj <- as.shiny.appobj(app)
|
||||
if (!is.null(appobj$onStart))
|
||||
appobj$onStart()
|
||||
# Ensure appobj$onStop() is called, and the current directory is restored,
|
||||
# regardless of whether invoking the server function is successful.
|
||||
tryCatch({
|
||||
server <- appobj$serverFuncSource()
|
||||
if (! "session" %in% names(formals(server)))
|
||||
stop("Tested application server functions must declare input, output, and session arguments.")
|
||||
body(server) <- rlang::expr({
|
||||
session$setEnv(base::environment())
|
||||
!!body(server)
|
||||
})
|
||||
if (length(args))
|
||||
stop("Arguments were provided to a server function.")
|
||||
withMockContext(server(input = session$input, output = session$output, session = session))
|
||||
}, finally = {
|
||||
if (!is.null(appobj$onStop))
|
||||
appobj$onStop()
|
||||
})
|
||||
|
||||
# If app is a server, we use only the server function's immediate
|
||||
# environment to construct the mask.
|
||||
mask <- rlang::new_data_mask(rlang::env_clone(session$env))
|
||||
withMockContext(rlang::eval_tidy(quosure, mask, rlang::caller_env()))
|
||||
withMockContext(session, rlang::eval_tidy(quosure, mask, rlang::caller_env()))
|
||||
return(invisible())
|
||||
}
|
||||
|
||||
if (is.null(app)) {
|
||||
path <- findEnclosingApp(".")
|
||||
app <- shinyAppDir(path)
|
||||
} else if (isServer(app)) {
|
||||
app <- shinyApp(fluidPage(), app)
|
||||
} else {
|
||||
app <- as.shiny.appobj(app)
|
||||
}
|
||||
|
||||
if (!is.null(app$onStart))
|
||||
app$onStart()
|
||||
if (!is.null(app$onStop))
|
||||
on.exit(app$onStop(), add = TRUE)
|
||||
|
||||
server <- app$serverFuncSource()
|
||||
if (!"session" %in% names(formals(server)))
|
||||
stop("Tested application server functions must declare input, output, and session arguments.")
|
||||
if (length(args))
|
||||
stop("Arguments were provided to a server function.")
|
||||
|
||||
body(server) <- rlang::expr({
|
||||
session$setEnv(base::environment())
|
||||
!!body(server)
|
||||
})
|
||||
withMockContext(session,
|
||||
server(input = session$input, output = session$output, session = session)
|
||||
)
|
||||
|
||||
# # If app is a server, we use only the server function's immediate
|
||||
# # environment to construct the mask.
|
||||
mask <- rlang::new_data_mask(rlang::env_clone(session$env))
|
||||
withMockContext(session, {
|
||||
rlang::eval_tidy(quosure, mask, rlang::caller_env())
|
||||
})
|
||||
invisible()
|
||||
}
|
||||
|
||||
withMockContext <- function(session, expr) {
|
||||
isolate(
|
||||
withReactiveDomain(session, {
|
||||
withr::with_options(list(`shiny.allowoutputreads` = TRUE), {
|
||||
# Sets a cache for renderCachedPlot() with cache = "app" to use.
|
||||
shinyOptions("cache" = session$appcache)
|
||||
expr
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# Helpers -----------------------------------------------------------------
|
||||
|
||||
isModuleServer <- function(x) {
|
||||
is.function(x) && names(formals(x))[[1]] == "id"
|
||||
}
|
||||
|
||||
isServer <- function(x) {
|
||||
if (!is.function(x)) {
|
||||
return(FALSE)
|
||||
}
|
||||
|
||||
if (length(formals(x)) < 3) {
|
||||
return(FALSE)
|
||||
}
|
||||
|
||||
identical(names(formals(x))[1:3], c("input", "output", "session"))
|
||||
}
|
||||
|
||||
119
R/update-input.R
119
R/update-input.R
@@ -1,8 +1,7 @@
|
||||
#' Change the value of a text input on the client
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param value The value to set for the input object.
|
||||
#' @param placeholder The placeholder to set for the input object.
|
||||
#' @inheritParams textInput
|
||||
#'
|
||||
#' @seealso [textInput()]
|
||||
#'
|
||||
@@ -35,7 +34,9 @@
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateTextInput <- function(session, inputId, label = NULL, value = NULL, placeholder = NULL) {
|
||||
updateTextInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL, placeholder = NULL) {
|
||||
validate_session_object(session)
|
||||
|
||||
message <- dropNulls(list(label=label, value=value, placeholder=placeholder))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
@@ -82,7 +83,7 @@ updateTextAreaInput <- updateTextInput
|
||||
#' Change the value of a checkbox input on the client
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param value The value to set for the input object.
|
||||
#' @inheritParams checkboxInput
|
||||
#'
|
||||
#' @seealso [checkboxInput()]
|
||||
#'
|
||||
@@ -107,7 +108,9 @@ updateTextAreaInput <- updateTextInput
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateCheckboxInput <- function(session, inputId, label = NULL, value = NULL) {
|
||||
updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL) {
|
||||
validate_session_object(session)
|
||||
|
||||
message <- dropNulls(list(label=label, value=value))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
@@ -116,8 +119,7 @@ updateCheckboxInput <- function(session, inputId, label = NULL, value = NULL) {
|
||||
#' Change the label or icon of an action button on the client
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param icon The icon to set for the input object. To remove the
|
||||
#' current icon, use `icon=character(0)`.
|
||||
#' @inheritParams actionButton
|
||||
#'
|
||||
#' @seealso [actionButton()]
|
||||
#'
|
||||
@@ -167,7 +169,9 @@ updateCheckboxInput <- function(session, inputId, label = NULL, value = NULL) {
|
||||
#' }
|
||||
#' @rdname updateActionButton
|
||||
#' @export
|
||||
updateActionButton <- function(session, inputId, label = NULL, icon = NULL) {
|
||||
updateActionButton <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL) {
|
||||
validate_session_object(session)
|
||||
|
||||
if (!is.null(icon)) icon <- as.character(validateIcon(icon))
|
||||
message <- dropNulls(list(label=label, icon=icon))
|
||||
session$sendInputMessage(inputId, message)
|
||||
@@ -180,12 +184,7 @@ updateActionLink <- updateActionButton
|
||||
#' Change the value of a date input on the client
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param value The desired date value. Either a Date object, or a string in
|
||||
#' `yyyy-mm-dd` format. Supply `NA` to clear the date.
|
||||
#' @param min The minimum allowed date. Either a Date object, or a string in
|
||||
#' `yyyy-mm-dd` format.
|
||||
#' @param max The maximum allowed date. Either a Date object, or a string in
|
||||
#' `yyyy-mm-dd` format.
|
||||
#' @inheritParams dateInput
|
||||
#'
|
||||
#' @seealso [dateInput()]
|
||||
#'
|
||||
@@ -213,8 +212,10 @@ updateActionLink <- updateActionButton
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateDateInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
min = NULL, max = NULL) {
|
||||
updateDateInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL,
|
||||
min = NULL, max = NULL)
|
||||
{
|
||||
validate_session_object(session)
|
||||
|
||||
value <- dateYMD(value, "value")
|
||||
min <- dateYMD(min, "min")
|
||||
@@ -228,14 +229,7 @@ updateDateInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
#' Change the start and end values of a date range input on the client
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param start The start date. Either a Date object, or a string in
|
||||
#' `yyyy-mm-dd` format. Supplying `NA` clears the start date.
|
||||
#' @param end The end date. Either a Date object, or a string in
|
||||
#' `yyyy-mm-dd` format. Supplying `NA` clears the end date.
|
||||
#' @param min The minimum allowed date. Either a Date object, or a string in
|
||||
#' `yyyy-mm-dd` format.
|
||||
#' @param max The maximum allowed date. Either a Date object, or a string in
|
||||
#' `yyyy-mm-dd` format.
|
||||
#' @inheritParams dateRangeInput
|
||||
#'
|
||||
#' @seealso [dateRangeInput()]
|
||||
#'
|
||||
@@ -265,9 +259,11 @@ updateDateInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateDateRangeInput <- function(session, inputId, label = NULL,
|
||||
updateDateRangeInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL,
|
||||
start = NULL, end = NULL, min = NULL,
|
||||
max = NULL) {
|
||||
max = NULL)
|
||||
{
|
||||
validate_session_object(session)
|
||||
|
||||
start <- dateYMD(start, "start")
|
||||
end <- dateYMD(end, "end")
|
||||
@@ -287,10 +283,10 @@ updateDateRangeInput <- function(session, inputId, label = NULL,
|
||||
#' Change the selected tab on the client
|
||||
#'
|
||||
#' @param session The `session` object passed to function given to
|
||||
#' `shinyServer`.
|
||||
#' `shinyServer`. Default is `getDefaultReactiveDomain()`.
|
||||
#' @param inputId The id of the `tabsetPanel`, `navlistPanel`,
|
||||
#' or `navbarPage` object.
|
||||
#' @param selected The name of the tab to make active.
|
||||
#' @inheritParams tabsetPanel
|
||||
#'
|
||||
#' @seealso [tabsetPanel()], [navlistPanel()],
|
||||
#' [navbarPage()]
|
||||
@@ -323,7 +319,9 @@ updateDateRangeInput <- function(session, inputId, label = NULL,
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateTabsetPanel <- function(session, inputId, selected = NULL) {
|
||||
updateTabsetPanel <- function(session = getDefaultReactiveDomain(), inputId, selected = NULL) {
|
||||
validate_session_object(session)
|
||||
|
||||
message <- dropNulls(list(value = selected))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
@@ -339,10 +337,7 @@ updateNavlistPanel <- updateTabsetPanel
|
||||
#' Change the value of a number input on the client
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param value The value to set for the input object.
|
||||
#' @param min Minimum value.
|
||||
#' @param max Maximum value.
|
||||
#' @param step Step size.
|
||||
#' @inheritParams numericInput
|
||||
#'
|
||||
#' @seealso [numericInput()]
|
||||
#'
|
||||
@@ -374,9 +369,11 @@ updateNavlistPanel <- updateTabsetPanel
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
updateNumericInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL,
|
||||
min = NULL, max = NULL, step = NULL) {
|
||||
|
||||
validate_session_object(session)
|
||||
|
||||
message <- dropNulls(list(
|
||||
label = label, value = formatNoSci(value),
|
||||
min = formatNoSci(min), max = formatNoSci(max), step = formatNoSci(step)
|
||||
@@ -389,12 +386,7 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
#' Change the value of a slider input on the client.
|
||||
#'
|
||||
#' @template update-input
|
||||
#' @param value The value to set for the input object.
|
||||
#' @param min Minimum value.
|
||||
#' @param max Maximum value.
|
||||
#' @param step Step size.
|
||||
#' @param timeFormat Date and POSIXt formatting.
|
||||
#' @param timezone The timezone offset for POSIXt objects.
|
||||
#' @inheritParams sliderInput
|
||||
#'
|
||||
#' @seealso [sliderInput()]
|
||||
#'
|
||||
@@ -426,9 +418,11 @@ updateNumericInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
#' )
|
||||
#' }
|
||||
#' @export
|
||||
updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
updateSliderInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL,
|
||||
min = NULL, max = NULL, step = NULL, timeFormat = NULL, timezone = NULL)
|
||||
{
|
||||
validate_session_object(session)
|
||||
|
||||
# If no min/max/value is provided, we won't know the
|
||||
# type, and this will return an empty string
|
||||
dataType <- getSliderType(min, max, value)
|
||||
@@ -461,6 +455,8 @@ updateSliderInput <- function(session, inputId, label = NULL, value = NULL,
|
||||
updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
|
||||
selected = NULL, inline = FALSE, type = NULL,
|
||||
choiceNames = NULL, choiceValues = NULL) {
|
||||
validate_session_object(session)
|
||||
|
||||
if (is.null(type)) stop("Please specify the type ('checkbox' or 'radio')")
|
||||
|
||||
args <- normalizeChoicesArgs(choices, choiceNames, choiceValues, mustExist = FALSE)
|
||||
@@ -518,9 +514,12 @@ updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateCheckboxGroupInput <- function(session, inputId, label = NULL,
|
||||
updateCheckboxGroupInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL,
|
||||
choices = NULL, selected = NULL, inline = FALSE,
|
||||
choiceNames = NULL, choiceValues = NULL) {
|
||||
choiceNames = NULL, choiceValues = NULL)
|
||||
{
|
||||
validate_session_object(session)
|
||||
|
||||
updateInputOptions(session, inputId, label, choices, selected,
|
||||
inline, "checkbox", choiceNames, choiceValues)
|
||||
}
|
||||
@@ -561,9 +560,12 @@ updateCheckboxGroupInput <- function(session, inputId, label = NULL,
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
|
||||
updateRadioButtons <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, choices = NULL,
|
||||
selected = NULL, inline = FALSE,
|
||||
choiceNames = NULL, choiceValues = NULL) {
|
||||
choiceNames = NULL, choiceValues = NULL)
|
||||
{
|
||||
validate_session_object(session)
|
||||
|
||||
# you must select at least one radio button
|
||||
if (is.null(selected)) {
|
||||
if (!is.null(choices)) selected <- choices[[1]]
|
||||
@@ -613,11 +615,14 @@ updateRadioButtons <- function(session, inputId, label = NULL, choices = NULL,
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
updateSelectInput <- function(session, inputId, label = NULL, choices = NULL,
|
||||
selected = NULL) {
|
||||
updateSelectInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, choices = NULL,
|
||||
selected = NULL)
|
||||
{
|
||||
validate_session_object(session)
|
||||
|
||||
choices <- if (!is.null(choices)) choicesWithNames(choices)
|
||||
if (!is.null(selected)) selected <- as.character(selected)
|
||||
options <- if (!is.null(choices)) selectOptions(choices, selected)
|
||||
options <- if (!is.null(choices)) selectOptions(choices, selected, inputId, FALSE)
|
||||
message <- dropNulls(list(label = label, options = options, value = selected))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
@@ -629,9 +634,12 @@ updateSelectInput <- function(session, inputId, label = NULL, choices = NULL,
|
||||
#' `choices` into the page at once (i.e., only use the client-side
|
||||
#' version of \pkg{selectize.js})
|
||||
#' @export
|
||||
updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
|
||||
updateSelectizeInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, choices = NULL,
|
||||
selected = NULL, options = list(),
|
||||
server = FALSE) {
|
||||
server = FALSE)
|
||||
{
|
||||
validate_session_object(session)
|
||||
|
||||
if (length(options)) {
|
||||
res <- checkAsIs(options)
|
||||
cfg <- tags$script(
|
||||
@@ -744,12 +752,15 @@ updateSelectizeInput <- function(session, inputId, label = NULL, choices = NULL,
|
||||
#' @rdname updateSelectInput
|
||||
#' @inheritParams varSelectInput
|
||||
#' @export
|
||||
updateVarSelectInput <- function(session, inputId, label = NULL, data = NULL, selected = NULL) {
|
||||
updateVarSelectInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, data = NULL, selected = NULL) {
|
||||
validate_session_object(session)
|
||||
|
||||
if (is.null(data)) {
|
||||
choices <- NULL
|
||||
} else {
|
||||
choices <- colnames(data)
|
||||
}
|
||||
|
||||
updateSelectInput(
|
||||
session = session,
|
||||
inputId = inputId,
|
||||
@@ -760,7 +771,11 @@ updateVarSelectInput <- function(session, inputId, label = NULL, data = NULL, se
|
||||
}
|
||||
#' @rdname updateSelectInput
|
||||
#' @export
|
||||
updateVarSelectizeInput <- function(session, inputId, label = NULL, data = NULL, selected = NULL, options = list(), server = FALSE) {
|
||||
updateVarSelectizeInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL,
|
||||
data = NULL, selected = NULL, options = list(), server = FALSE)
|
||||
{
|
||||
validate_session_object(session)
|
||||
|
||||
if (is.null(data)) {
|
||||
choices <- NULL
|
||||
} else {
|
||||
|
||||
110
R/utils-lang.R
Normal file
110
R/utils-lang.R
Normal file
@@ -0,0 +1,110 @@
|
||||
# Given a list of quosures, return a function that will evaluate them and return
|
||||
# a list of resulting values. If the list contains a single expression, unwrap
|
||||
# it from the list.
|
||||
quos_to_func <- function(qs) {
|
||||
if (length(qs) == 0) {
|
||||
stop("Need at least one item in `...` to use as cache key or event.")
|
||||
}
|
||||
|
||||
if (length(qs) == 1) {
|
||||
# Special case for one quosure. This is needed for async to work -- that is,
|
||||
# when the quosure returns a promise. It needs to not be wrapped into a list
|
||||
# for the hybrid_chain stuff to detect that it's a promise. (Plus, it's not
|
||||
# even clear what it would mean to mix promises and non-promises in the
|
||||
# key.)
|
||||
qs <- qs[[1]]
|
||||
function() {
|
||||
eval_tidy(qs)
|
||||
}
|
||||
|
||||
} else {
|
||||
function() {
|
||||
lapply(qs, eval_tidy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Given a list of quosures, return a string representation of the expressions.
|
||||
#
|
||||
# qs <- list(quo(a+1), quo({ b+2; b + 3 }))
|
||||
# quos_to_label(qs)
|
||||
# #> [1] "a + 1, {\n b + 2\n b + 3\n}"
|
||||
quos_to_label <- function(qs) {
|
||||
res <- lapply(qs, function(q) {
|
||||
paste(deparse(get_expr(q)), collapse = "\n")
|
||||
})
|
||||
|
||||
paste(res, collapse = ", ")
|
||||
}
|
||||
|
||||
# Get the formals and body for a function, without source refs. This is used for
|
||||
# consistent hashing of the function.
|
||||
formalsAndBody <- function(x) {
|
||||
if (is.null(x)) {
|
||||
return(list())
|
||||
}
|
||||
|
||||
list(
|
||||
formals = formals(x),
|
||||
body = body(zap_srcref(x))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# This function is to be called from functions like `reactive()`, `observe()`,
|
||||
# and the various render functions. It handles the following cases:
|
||||
# - The typical case where x is an unquoted expression, and `env` and `quoted`
|
||||
# are not used.
|
||||
# - New-style metaprogramming cases, where rlang::inject() is used to inline a
|
||||
# quosure into the AST, as in `inject(reactive(!!x))`.
|
||||
# - Old-style metaprogramming cases, where `env` and/or `quoted` are used.
|
||||
#
|
||||
# Much of the complexity is handling old-style metaprogramming cases. The code
|
||||
# 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
|
||||
# calling function, it would look like this:
|
||||
#
|
||||
# if (!missing(env) || !missing(quoted)) {
|
||||
# deprecatedEnvQuotedMessage()
|
||||
# if (!quoted) x <- substitute(x)
|
||||
# x <- new_quosure(x, env)
|
||||
#
|
||||
# } else {
|
||||
# x <- substitute(x)
|
||||
# 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())
|
||||
}
|
||||
x <- new_quosure(x, env)
|
||||
|
||||
} else {
|
||||
x <- eval(substitute(substitute(x)), parent.frame())
|
||||
|
||||
# At this point, x can be a quosure if rlang::inject() is used, but the
|
||||
# typical case is that x is not a quosure.
|
||||
if (!is_quosure(x)) {
|
||||
x <- new_quosure(x, env = parent.frame(2L))
|
||||
}
|
||||
}
|
||||
|
||||
x
|
||||
}
|
||||
358
R/utils.R
358
R/utils.R
@@ -113,24 +113,6 @@ isWholeNum <- function(x, tol = .Machine$double.eps^0.5) {
|
||||
abs(x - round(x)) < tol
|
||||
}
|
||||
|
||||
`%OR%` <- function(x, y) {
|
||||
if (is.null(x) || isTRUE(is.na(x)))
|
||||
y
|
||||
else
|
||||
x
|
||||
}
|
||||
|
||||
`%AND%` <- function(x, y) {
|
||||
if (!is.null(x) && !isTRUE(is.na(x)))
|
||||
if (!is.null(y) && !isTRUE(is.na(y)))
|
||||
return(y)
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
`%.%` <- function(x, y) {
|
||||
paste(x, y, sep='')
|
||||
}
|
||||
|
||||
# Given a vector or list, drop all the NULL items in it
|
||||
dropNulls <- function(x) {
|
||||
x[!vapply(x, is.null, FUN.VALUE=logical(1))]
|
||||
@@ -182,6 +164,10 @@ asNamed <- function(x) {
|
||||
x
|
||||
}
|
||||
|
||||
empty_named_list <- function() {
|
||||
list(a = 1)[0]
|
||||
}
|
||||
|
||||
# Given two named vectors, join them together, and keep only the last element
|
||||
# with a given name in the resulting vector. If b has any elements with the same
|
||||
# name as elements in a, the element in a is dropped. Also, if there are any
|
||||
@@ -220,6 +206,7 @@ sort_c <- function(x, ...) {
|
||||
sort(x, method = "radix", ...)
|
||||
}
|
||||
|
||||
|
||||
# Wrapper around list2env with a NULL check. In R <3.2.0, if an empty unnamed
|
||||
# list is passed to list2env(), it errors. But an empty named list is OK. For
|
||||
# R >=3.2.0, this wrapper is not necessary.
|
||||
@@ -427,7 +414,8 @@ makeFunction <- function(args = pairlist(), body, env = parent.frame()) {
|
||||
#' 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.
|
||||
#' 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
|
||||
@@ -485,7 +473,8 @@ exprToFunction <- function(expr, env=parent.frame(), quoted=FALSE) {
|
||||
#' 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.
|
||||
#' 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
|
||||
@@ -531,6 +520,48 @@ installExprFunction <- function(expr, name, eval.env = parent.frame(2),
|
||||
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
|
||||
#'
|
||||
#' Returns a named list of key-value pairs.
|
||||
@@ -631,37 +662,6 @@ shinyCallingHandlers <- function(expr) {
|
||||
)
|
||||
}
|
||||
|
||||
#' Print message for deprecated functions in Shiny
|
||||
#'
|
||||
#' To disable these messages, use `options(shiny.deprecation.messages=FALSE)`.
|
||||
#'
|
||||
#' @param new Name of replacement function.
|
||||
#' @param msg Message to print. If used, this will override the default message.
|
||||
#' @param old Name of deprecated function.
|
||||
#' @param version The last version of Shiny before the item was deprecated.
|
||||
#' @keywords internal
|
||||
shinyDeprecated <- function(new=NULL, msg=NULL,
|
||||
old=as.character(sys.call(sys.parent()))[1L],
|
||||
version = NULL) {
|
||||
|
||||
if (getOption("shiny.deprecation.messages") %OR% TRUE == FALSE)
|
||||
return(invisible())
|
||||
|
||||
if (is.null(msg)) {
|
||||
msg <- paste(old, "is deprecated.")
|
||||
if (!is.null(new)) {
|
||||
msg <- paste(msg, "Please use", new, "instead.",
|
||||
"To disable this message, run options(shiny.deprecation.messages=FALSE)")
|
||||
}
|
||||
}
|
||||
|
||||
if (!is.null(version)) {
|
||||
msg <- paste0(msg, " (Last used in version ", version, ")")
|
||||
}
|
||||
|
||||
# Similar to .Deprecated(), but print a message instead of warning
|
||||
message(msg)
|
||||
}
|
||||
|
||||
#' Register a function with the debugger (if one is active).
|
||||
#'
|
||||
@@ -1111,52 +1111,39 @@ reactiveStop <- function(message = "", class = NULL) {
|
||||
|
||||
#' Validate input values and other conditions
|
||||
#'
|
||||
#' For an output rendering function (e.g. [renderPlot()]), you may
|
||||
#' need to check that certain input values are available and valid before you
|
||||
#' can render the output. `validate` gives you a convenient mechanism for
|
||||
#' doing so.
|
||||
#' @description
|
||||
#' `validate()` provides convenient mechanism for validating that an output
|
||||
#' has all the inputs necessary for successful rendering. It takes any number
|
||||
#' of (unnamed) arguments, each representing a condition to test. If any
|
||||
#' of condition fails (i.e. is not ["truthy"][isTruthy]), a special type of
|
||||
#' error is signaled to stop execution. If this error is not handled by
|
||||
#' application-specific code, it is displayed to the user by Shiny.
|
||||
#'
|
||||
#' The `validate` function takes any number of (unnamed) arguments, each of
|
||||
#' which represents a condition to test. If any of the conditions represent
|
||||
#' failure, then a special type of error is signaled which stops execution. If
|
||||
#' this error is not handled by application-specific code, it is displayed to
|
||||
#' the user by Shiny.
|
||||
#' If you use `validate()` in a [reactive()] validation failures will
|
||||
#' automatically propagate to outputs that use the reactive.
|
||||
#'
|
||||
#' An easy way to provide arguments to `validate` is to use the `need`
|
||||
#' function, which takes an expression and a string; if the expression is
|
||||
#' considered a failure, then the string will be used as the error message. The
|
||||
#' `need` function considers its expression to be a failure if it is any of
|
||||
#' the following:
|
||||
#' @section `need()`:
|
||||
#' An easy way to provide arguments to `validate()` is to use `need()`, which
|
||||
#' takes an expression and a string. If the expression is not
|
||||
#' ["truthy"][isTruthy] then the string will be used as the error message.
|
||||
#'
|
||||
#' \itemize{
|
||||
#' \item{`FALSE`}
|
||||
#' \item{`NULL`}
|
||||
#' \item{`""`}
|
||||
#' \item{An empty atomic vector}
|
||||
#' \item{An atomic vector that contains only missing values}
|
||||
#' \item{A logical vector that contains all `FALSE` or missing values}
|
||||
#' \item{An object of class `"try-error"`}
|
||||
#' \item{A value that represents an unclicked [actionButton()]}
|
||||
#' If "truthiness" is flexible for your use case, you'll need to explicitly
|
||||
#' generate a logical values. For example, if you want allow `NA` but not
|
||||
#' `NULL`, you can `!is.null(input$foo)`.
|
||||
#'
|
||||
#' If you need validation logic that differs significantly from `need()`, you
|
||||
#' can create your own validation test functions. A passing test should return
|
||||
#' `NULL`. A failing test should return either a string providing the error
|
||||
#' to display to the user, or if the failure should happen silently, `FALSE`.
|
||||
#'
|
||||
#' Alternatively you can use `validate()` within an `if` statement, which is
|
||||
#' particularly useful for more complex conditions:
|
||||
#'
|
||||
#' ```
|
||||
#' if (input$x < 0 && input$choice == "positive") {
|
||||
#' validate("If choice is positive then x must be greater than 0")
|
||||
#' }
|
||||
#'
|
||||
#' If any of these values happen to be valid, you can explicitly turn them to
|
||||
#' logical values. For example, if you allow `NA` but not `NULL`, you
|
||||
#' can use the condition `!is.null(input$foo)`, because `!is.null(NA)
|
||||
#' == TRUE`.
|
||||
#'
|
||||
#' If you need validation logic that differs significantly from `need`, you
|
||||
#' can create other validation test functions. A passing test should return
|
||||
#' `NULL`. A failing test should return an error message as a
|
||||
#' single-element character vector, or if the failure should happen silently,
|
||||
#' `FALSE`.
|
||||
#'
|
||||
#' Because validation failure is signaled as an error, you can use
|
||||
#' `validate` in reactive expressions, and validation failures will
|
||||
#' automatically propagate to outputs that use the reactive expression. In
|
||||
#' other words, if reactive expression `a` needs `input$x`, and two
|
||||
#' outputs use `a` (and thus depend indirectly on `input$x`), it's
|
||||
#' not necessary for the outputs to validate `input$x` explicitly, as long
|
||||
#' as `a` does validate it.
|
||||
#' ```
|
||||
#'
|
||||
#' @param ... A list of tests. Each test should equal `NULL` for success,
|
||||
#' `FALSE` for silent failure, or a string for failure with an error
|
||||
@@ -1233,7 +1220,7 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
|
||||
|
||||
#' Check for required values
|
||||
#'
|
||||
#' Ensure that values are available ("truthy"--see Details) before proceeding
|
||||
#' Ensure that values are available (["truthy"][isTruthy]) before proceeding
|
||||
#' with a calculation or action. If any of the given values is not truthy, the
|
||||
#' operation is stopped by raising a "silent" exception (not logged by Shiny,
|
||||
#' nor displayed in the Shiny app's UI).
|
||||
@@ -1242,11 +1229,13 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
|
||||
#' is to call it like a statement (ignoring its return value) before attempting
|
||||
#' operations using the required values:
|
||||
#'
|
||||
#' \preformatted{rv <- reactiveValues(state = FALSE)
|
||||
#' ```
|
||||
#' rv <- reactiveValues(state = FALSE)
|
||||
#' r <- reactive({
|
||||
#' req(input$a, input$b, rv$state)
|
||||
#' # Code that uses input$a, input$b, and/or rv$state...
|
||||
#' })}
|
||||
#' })
|
||||
#' ```
|
||||
#'
|
||||
#' In this example, if `r()` is called and any of `input$a`,
|
||||
#' `input$b`, and `rv$state` are `NULL`, `FALSE`, `""`,
|
||||
@@ -1255,54 +1244,21 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
|
||||
#'
|
||||
#' The second is to use it to wrap an expression that must be truthy:
|
||||
#'
|
||||
#' \preformatted{output$plot <- renderPlot({
|
||||
#' ```
|
||||
#' output$plot <- renderPlot({
|
||||
#' if (req(input$plotType) == "histogram") {
|
||||
#' hist(dataset())
|
||||
#' } else if (input$plotType == "scatter") {
|
||||
#' qplot(dataset(), aes(x = x, y = y))
|
||||
#' }
|
||||
#' })}
|
||||
#' })
|
||||
#' ```
|
||||
#'
|
||||
#' In this example, `req(input$plotType)` first checks that
|
||||
#' `input$plotType` is truthy, and if so, returns it. This is a convenient
|
||||
#' way to check for a value "inline" with its first use.
|
||||
#'
|
||||
#' **Truthy and falsy values**
|
||||
#'
|
||||
#' The terms "truthy" and "falsy" generally indicate whether a value, when
|
||||
#' coerced to a [base::logical()], is `TRUE` or `FALSE`. We use
|
||||
#' the term a little loosely here; our usage tries to match the intuitive
|
||||
#' notions of "Is this value missing or available?", or "Has the user provided
|
||||
#' an answer?", or in the case of action buttons, "Has the button been
|
||||
#' clicked?".
|
||||
#'
|
||||
#' For example, a `textInput` that has not been filled out by the user has
|
||||
#' a value of `""`, so that is considered a falsy value.
|
||||
#'
|
||||
#' To be precise, `req` considers a value truthy *unless* it is one
|
||||
#' of:
|
||||
#'
|
||||
#' \itemize{
|
||||
#' \item{`FALSE`}
|
||||
#' \item{`NULL`}
|
||||
#' \item{`""`}
|
||||
#' \item{An empty atomic vector}
|
||||
#' \item{An atomic vector that contains only missing values}
|
||||
#' \item{A logical vector that contains all `FALSE` or missing values}
|
||||
#' \item{An object of class `"try-error"`}
|
||||
#' \item{A value that represents an unclicked [actionButton()]}
|
||||
#' }
|
||||
#'
|
||||
#' Note in particular that the value `0` is considered truthy, even though
|
||||
#' `as.logical(0)` is `FALSE`.
|
||||
#'
|
||||
#' If the built-in rules for truthiness do not match your requirements, you can
|
||||
#' always work around them. Since `FALSE` is falsy, you can simply provide
|
||||
#' the results of your own checks to `req`:
|
||||
#'
|
||||
#' `req(input$a != 0)`
|
||||
#'
|
||||
#' **Using `req(FALSE)`**
|
||||
#' @section Using `req(FALSE)`:
|
||||
#'
|
||||
#' 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
|
||||
@@ -1310,7 +1266,7 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
|
||||
#' if you have a complicated condition to check for (or perhaps if you'd like to
|
||||
#' divide your condition into nested `if` statements).
|
||||
#'
|
||||
#' **Using `cancelOutput = TRUE`**
|
||||
#' @section Using `cancelOutput = TRUE`:
|
||||
#'
|
||||
#' When `req(..., cancelOutput = TRUE)` is used, the "silent" exception is
|
||||
#' also raised, but it is treated slightly differently if one or more outputs are
|
||||
@@ -1329,7 +1285,6 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
|
||||
#' @param cancelOutput If `TRUE` and an output is being evaluated, stop
|
||||
#' processing as usual but instead of clearing the output, leave it in
|
||||
#' whatever state it happens to be in.
|
||||
#' @param x An expression whose truthiness value we want to determine
|
||||
#' @return The first value that was passed in.
|
||||
#' @export
|
||||
#' @examples
|
||||
@@ -1419,14 +1374,40 @@ cancelOutput <- function() {
|
||||
#
|
||||
# Can be used to facilitate short-circuit eval on dots.
|
||||
dotloop <- function(fun_, ...) {
|
||||
for (i in 1:(nargs()-1)) {
|
||||
for (i in seq_len(nargs() - 1)) {
|
||||
fun_(eval(as.symbol(paste0("..", i))))
|
||||
}
|
||||
invisible()
|
||||
}
|
||||
|
||||
#' Truthy and falsy values
|
||||
#'
|
||||
#' The terms "truthy" and "falsy" generally indicate whether a value, when
|
||||
#' coerced to a [base::logical()], is `TRUE` or `FALSE`. We use
|
||||
#' the term a little loosely here; our usage tries to match the intuitive
|
||||
#' notions of "Is this value missing or available?", or "Has the user provided
|
||||
#' an answer?", or in the case of action buttons, "Has the button been
|
||||
#' clicked?".
|
||||
#'
|
||||
#' For example, a `textInput` that has not been filled out by the user has
|
||||
#' a value of `""`, so that is considered a falsy value.
|
||||
#'
|
||||
#' To be precise, a value is truthy *unless* it is one of:
|
||||
#'
|
||||
#' * `FALSE`
|
||||
#' * `NULL`
|
||||
#' * `""`
|
||||
#' * An empty atomic vector
|
||||
#' * An atomic vector that contains only missing values
|
||||
#' * A logical vector that contains all `FALSE` or missing values
|
||||
#' * An object of class `"try-error"`
|
||||
#' * A value that represents an unclicked [actionButton()]
|
||||
#'
|
||||
#' Note in particular that the value `0` is considered truthy, even though
|
||||
#' `as.logical(0)` is `FALSE`.
|
||||
#'
|
||||
#' @param x An expression whose truthiness value we want to determine
|
||||
#' @export
|
||||
#' @rdname req
|
||||
isTruthy <- function(x) {
|
||||
if (inherits(x, 'try-error'))
|
||||
return(FALSE)
|
||||
@@ -1503,7 +1484,7 @@ checkEncoding <- function(file) {
|
||||
if (identical(charToRaw(readChar(file, 3L, TRUE)), charToRaw('\UFEFF'))) {
|
||||
warning('You should not include the Byte Order Mark (BOM) in ', file, '. ',
|
||||
'Please re-save it in UTF-8 without BOM. See ',
|
||||
'http://shiny.rstudio.com/articles/unicode.html for more info.')
|
||||
'https://shiny.rstudio.com/articles/unicode.html for more info.')
|
||||
return('UTF-8-BOM')
|
||||
}
|
||||
x <- readChar(file, size, useBytes = TRUE)
|
||||
@@ -1587,15 +1568,19 @@ URLencode <- function(value, reserved = FALSE) {
|
||||
if (reserved) encodeURIComponent(value) else encodeURI(value)
|
||||
}
|
||||
|
||||
# Make user-supplied dates are either NULL or can be coerced
|
||||
# to a yyyy-mm-dd formatted string. If a date is specified, this
|
||||
# function returns a string for consistency across locales.
|
||||
# Also, `as.Date()` is used to coerce strings to date objects
|
||||
# so that strings like "2016-08-9" are expanded to "2016-08-09"
|
||||
# Make sure user-supplied dates are either NULL or can be coerced to a
|
||||
# yyyy-mm-dd formatted string. If a date is specified, this function returns a
|
||||
# string for consistency across locales. Also, `as.Date()` is used to coerce
|
||||
# strings to date objects so that strings like "2016-08-9" are expanded to
|
||||
# "2016-08-09". If any of the values result in error or NA, then the input
|
||||
# `date` is returned unchanged.
|
||||
dateYMD <- function(date = NULL, argName = "value") {
|
||||
if (!length(date)) return(NULL)
|
||||
if (length(date) > 1) warning("Expected `", argName, "` to be of length 1.")
|
||||
tryCatch(date <- format(as.Date(date), "%Y-%m-%d"),
|
||||
tryCatch({
|
||||
res <- format(as.Date(date), "%Y-%m-%d")
|
||||
if (any(is.na(res))) stop()
|
||||
date <- res
|
||||
},
|
||||
error = function(e) {
|
||||
warning(
|
||||
"Couldn't coerce the `", argName,
|
||||
@@ -1618,18 +1603,17 @@ wrapFunctionLabel <- function(func, name, ..stacktraceon = FALSE) {
|
||||
assign(name, func, environment())
|
||||
registerDebugHook(name, environment(), name)
|
||||
|
||||
relabelWrapper <- eval(substitute(
|
||||
function(...) {
|
||||
# This `f` gets renamed to the value of `name`. Note that it may not
|
||||
# print as the new name, because of source refs stored in the function.
|
||||
if (..stacktraceon)
|
||||
..stacktraceon..(f(...))
|
||||
else
|
||||
f(...)
|
||||
},
|
||||
list(f = as.name(name))
|
||||
))
|
||||
if (..stacktraceon) {
|
||||
# We need to wrap the `...` in `!!quote(...)` so that R CMD check won't
|
||||
# complain about "... may be used in an incorrect context"
|
||||
body <- expr({ ..stacktraceon..((!!name)(!!quote(...))) })
|
||||
} else {
|
||||
body <- expr({ (!!name)(!!quote(...)) })
|
||||
}
|
||||
relabelWrapper <- new_function(pairlist2(... =), body, environment())
|
||||
|
||||
# Preserve the original function that was passed in; is used for caching.
|
||||
attr(relabelWrapper, "wrappedFunc") <- func
|
||||
relabelWrapper
|
||||
}
|
||||
|
||||
@@ -1689,19 +1673,23 @@ hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
|
||||
if (promises::is.promising(result$value)) {
|
||||
# Purposefully NOT including domain (nor replace), as we're already in
|
||||
# the domain at this point
|
||||
p <- promise_chain(setVisible(result), ..., catch = catch, finally = finally)
|
||||
p <- promise_chain(valueWithVisible(result), ..., catch = catch, finally = finally)
|
||||
runFinally <- FALSE
|
||||
p
|
||||
} else {
|
||||
result <- Reduce(function(v, func) {
|
||||
if (".visible" %in% names(formals(func))) {
|
||||
withVisible(func(v$value, .visible = v$visible))
|
||||
} else {
|
||||
withVisible(func(v$value))
|
||||
}
|
||||
}, list(...), result)
|
||||
result <- Reduce(
|
||||
function(v, func) {
|
||||
if (v$visible) {
|
||||
withVisible(func(v$value))
|
||||
} else {
|
||||
withVisible(func(invisible(v$value)))
|
||||
}
|
||||
},
|
||||
list(...),
|
||||
result
|
||||
)
|
||||
|
||||
setVisible(result)
|
||||
valueWithVisible(result)
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -1722,24 +1710,13 @@ hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
|
||||
}
|
||||
}
|
||||
|
||||
# Returns `value` with either `invisible()` applied or not, depending on the
|
||||
# value of `visible`.
|
||||
#
|
||||
# If the `visible` is missing, then `value` should be a list as returned from
|
||||
# `withVisible()`, and that visibility will be applied.
|
||||
setVisible <- function(value, visible) {
|
||||
if (missing(visible)) {
|
||||
visible <- value$visible
|
||||
value <- value$value
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
invisible(value)
|
||||
} else {
|
||||
(value)
|
||||
}
|
||||
# Given a list with items named `value` and `visible`, return `x$value` either
|
||||
# visibly, or invisibly, depending on the value of `x$visible`.
|
||||
valueWithVisible <- function(x) {
|
||||
if (x$visible) x$value else invisible(x$value)
|
||||
}
|
||||
|
||||
|
||||
createVarPromiseDomain <- function(env, name, value) {
|
||||
force(env)
|
||||
force(name)
|
||||
@@ -1783,7 +1760,10 @@ getSliderType <- function(min, max, value) {
|
||||
else "number"
|
||||
}))
|
||||
if (length(type) > 1) {
|
||||
stop("Type mismatch for `min`, `max`, and `value`. Each must be Date, POSIXt, or number.")
|
||||
rlang::abort(c(
|
||||
"Type mismatch for `min`, `max`, and `value`.",
|
||||
"All values must either be numeric, Date, or POSIXt."
|
||||
))
|
||||
}
|
||||
type[[1]]
|
||||
}
|
||||
@@ -1893,3 +1873,15 @@ is_available <- function(package, version = NULL) {
|
||||
}
|
||||
installed && isTRUE(utils::packageVersion(package) >= version)
|
||||
}
|
||||
|
||||
|
||||
# cached version of utils::packageVersion("shiny")
|
||||
shinyPackageVersion <- local({
|
||||
version <- NULL
|
||||
function() {
|
||||
if (is.null(version)) {
|
||||
version <<- utils::packageVersion("shiny")
|
||||
}
|
||||
version
|
||||
}
|
||||
})
|
||||
|
||||
2
R/version_jquery.R
Normal file
2
R/version_jquery.R
Normal file
@@ -0,0 +1,2 @@
|
||||
# Generated by tools/updatejQuery.R; do not edit by hand
|
||||
version_jquery <- "3.6.0"
|
||||
56
R/viewer.R
Normal file
56
R/viewer.R
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
#' Viewer options
|
||||
#'
|
||||
#' Use these functions to control where the gadget is displayed in RStudio (or
|
||||
#' other R environments that emulate RStudio's viewer pane/dialog APIs). If
|
||||
#' viewer APIs are not available in the current R environment, then the gadget
|
||||
#' will be displayed in the system's default web browser (see
|
||||
#' [utils::browseURL()]).
|
||||
#'
|
||||
#' @return A function that takes a single `url` parameter, suitable for
|
||||
#' passing as the `viewer` argument of [runGadget()].
|
||||
#'
|
||||
#' @rdname viewer
|
||||
#' @name viewer
|
||||
NULL
|
||||
|
||||
#' @param minHeight The minimum height (in pixels) desired to show the gadget in
|
||||
#' the viewer pane. If a positive number, resize the pane if necessary to show
|
||||
#' at least that many pixels. If `NULL`, use the existing viewer pane
|
||||
#' size. If `"maximize"`, use the maximum available vertical space.
|
||||
#' @rdname viewer
|
||||
#' @export
|
||||
paneViewer <- function(minHeight = NULL) {
|
||||
viewer <- getOption("viewer")
|
||||
if (is.null(viewer)) {
|
||||
utils::browseURL
|
||||
} else {
|
||||
function(url) {
|
||||
viewer(url, minHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#' @param dialogName The window title to display for the dialog.
|
||||
#' @param width,height The desired dialog width/height, in pixels.
|
||||
#' @rdname viewer
|
||||
#' @export
|
||||
dialogViewer <- function(dialogName, width = 600, height = 600) {
|
||||
viewer <- getOption("shinygadgets.showdialog")
|
||||
if (is.null(viewer)) {
|
||||
utils::browseURL
|
||||
} else {
|
||||
function(url) {
|
||||
viewer(dialogName, url, width = width, height = height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#' @param browser See [utils::browseURL()].
|
||||
#' @rdname viewer
|
||||
#' @export
|
||||
browserViewer <- function(browser = getOption("browser")) {
|
||||
function(url) {
|
||||
utils::browseURL(url, browser = browser)
|
||||
}
|
||||
}
|
||||
65
README.md
65
README.md
@@ -7,62 +7,51 @@
|
||||
|
||||
<!-- badges: end -->
|
||||
|
||||
Shiny is a new package from RStudio that makes it incredibly easy to build interactive web applications with R.
|
||||
|
||||
For an introduction and examples, visit the [Shiny Dev Center](http://shiny.rstudio.com/).
|
||||
|
||||
If you have general questions about using Shiny, please use the [RStudio Community website](https://community.rstudio.com). For bug reports, please use the [issue tracker](https://github.com/rstudio/shiny/issues).
|
||||
Easily build rich and productive interactive web apps in R — no HTML/CSS/JavaScript required.
|
||||
|
||||
## Features
|
||||
|
||||
* Build useful web applications with only a few lines of code—no JavaScript required.
|
||||
* Shiny applications are automatically "live" in the same way that spreadsheets are live. Outputs change instantly as users modify inputs, without requiring a reload of the browser.
|
||||
* Shiny user interfaces can be built entirely using R, or can be written directly in HTML, CSS, and JavaScript for more flexibility.
|
||||
* Works in any R environment (Console R, Rgui for Windows or Mac, ESS, StatET, RStudio, etc.).
|
||||
* Attractive default UI theme based on [Bootstrap](http://getbootstrap.com/).
|
||||
* A highly customizable slider widget with built-in support for animation.
|
||||
* Prebuilt output widgets for displaying plots, tables, and printed output of R objects.
|
||||
* Fast bidirectional communication between the web browser and R using the [httpuv](https://github.com/rstudio/httpuv) package.
|
||||
* Uses a [reactive](http://en.wikipedia.org/wiki/Reactive_programming) programming model that eliminates messy event handling code, so you can focus on the code that really matters.
|
||||
* Develop and redistribute your own Shiny widgets that other developers can easily drop into their own applications (coming soon!).
|
||||
* An intuitive and extensible [reactive programming](https://en.wikipedia.org/wiki/Reactive_programming) model which makes it easy to transform existing R code into a "live app" where outputs automatically react to new user input.
|
||||
* Compared to event-based programming, reactivity allows Shiny to do the minimum amount of work when input(s) change, and allows humans to more easily reason about complex [MVC logic](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller).
|
||||
* 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.
|
||||
* 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://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.
|
||||
* 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.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the stable version from CRAN, simply run the following from an R console:
|
||||
To install the stable version from CRAN:
|
||||
|
||||
```r
|
||||
install.packages("shiny")
|
||||
```
|
||||
|
||||
To install the latest development builds directly from GitHub, run this instead:
|
||||
|
||||
```r
|
||||
if (!require("remotes"))
|
||||
install.packages("remotes")
|
||||
remotes::install_github("rstudio/shiny")
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
To learn more we highly recommend you check out the [Shiny Tutorial](http://shiny.rstudio.com/tutorial/). The tutorial explains the framework in-depth, walks you through building a simple application, and includes extensive annotated examples.
|
||||
Once installed, load the library and run an example:
|
||||
|
||||
## Bootstrap 3 migration
|
||||
|
||||
Shiny versions 0.10.2.2 and below used the Bootstrap 2 web framework. After 0.10.2.2, Shiny switched to Bootstrap 3. For most users, the upgrade should be seamless. However, if you have have customized your HTML-generating code to use features specific to Bootstrap 2, you may need to update your code to work with Bootstrap 3.
|
||||
|
||||
If you do not wish to update your code at this time, you can use the [shinybootstrap2](https://github.com/rstudio/shinybootstrap2) package for backward compatibility.
|
||||
|
||||
If you prefer to install an older version of Shiny, you can do it using the devtools package:
|
||||
|
||||
```R
|
||||
devtools::install_version("shiny", version = "0.10.2.2")
|
||||
```r
|
||||
library(shiny)
|
||||
# Launches an app, with the app's source code included
|
||||
runExample("06_tabsets")
|
||||
# Lists more prepackaged examples
|
||||
runExample()
|
||||
```
|
||||
|
||||
## Development notes
|
||||
For more examples and inspiration, check out the [Shiny User Gallery](https://shiny.rstudio.com/gallery/).
|
||||
|
||||
The Javascript code in Shiny is minified using tools that run on Node.js. See the tools/ directory for more information.
|
||||
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.
|
||||
|
||||
## Guidelines for contributing
|
||||
## Getting Help
|
||||
|
||||
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.
|
||||
|
||||
## Contributing
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
library(shinytest)
|
||||
shinytest::testApp("../")
|
||||
|
||||
expect_pass(testApp("../", suffix = osName()))
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
context("exampleModuleServer")
|
||||
|
||||
# See ?testServer for more information
|
||||
testServer(exampleModuleServer, {
|
||||
# Set initial value of a button
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
context("app")
|
||||
|
||||
testServer(expr = {
|
||||
# Set the `size` slider and check the output
|
||||
session$setInputs(size = 6)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
# Test the lexical_sort function from R/example.R
|
||||
context("sort")
|
||||
|
||||
test_that("Lexical sorting works", {
|
||||
expect_equal(lexical_sort(c(1, 2, 3)), c(1, 2, 3))
|
||||
expect_equal(lexical_sort(c(1, 2, 3, 13, 11, 21)), c(1, 11, 13, 2, 21, 3))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html{{ if (isTRUE(nzchar(lang))) paste0(" lang=\"", lang, "\"") }}>
|
||||
<head>
|
||||
{{ headContent() }}
|
||||
</head>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<head lang = "en">
|
||||
<title>An error has occurred</title>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
.btn:focus{outline:dotted 2px #000}div.active:focus{outline:dotted 1px #000}a:focus{outline:dotted 1px #000}.close:hover,.close:focus{outline:dotted 1px #000}.nav>li>a:hover,.nav>li>a:focus{outline:dotted 1px #000}.carousel-indicators li,.carousel-indicators li.active{height:18px;width:18px;border-width:2px;position:relative;box-shadow:0px 0px 0px 1px #808080}.carousel-indicators.active li{background-color:rgba(100,149,253,0.6)}.carousel-indicators.active li.active{background-color:white}.carousel-tablist-highlight{display:block;position:absolute;outline:2px solid transparent;background-color:transparent;box-shadow:0px 0px 0px 1px transparent}.carousel-tablist-highlight.focus{outline:2px solid #6495ED;background-color:rgba(0,0,0,0.4)}a.carousel-control:focus{outline:2px solid #6495ED;background-image:linear-gradient(to right, transparent 0px, rgba(0,0,0,0.5) 100%);box-shadow:0px 0px 0px 1px #000000}.carousel-pause-button{position:absolute;top:-30em;left:-300em;display:block}.carousel-pause-button.focus{top:0.5em;left:0.5em}.carousel:hover .carousel-caption,.carousel.contrast .carousel-caption{background-color:rgba(0,0,0,0.5);z-index:10}.alert-success{color:#2d4821}.alert-info{color:#214c62}.alert-warning{color:#6c4a00;background-color:#f9f1c6}.alert-danger{color:#d2322d}.alert-danger:hover{color:#a82824}
|
||||
1
inst/www/shared/bootstrap/accessibility/css/bootstrap-accessibility.min.css
vendored
Normal file
1
inst/www/shared/bootstrap/accessibility/css/bootstrap-accessibility.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.btn:focus{outline:none 2px #000}.btn:focus-visible{outline:auto 2px #000}div.active:focus{outline:none 1px #000}div.active:focus-visible{outline:auto 1px #000}a:focus{outline:none 1px #000}a:focus-visible{outline:auto 1px #000}.close:hover,.close:focus{outline:none 1px #000}.close:focus-visible{outline:auto 1px #000}.nav>li>a:hover,.nav>li>a:focus{outline:none 1px #000}.nav>li>a:focus-visible{outline:auto 1px #000}.carousel-indicators li,.carousel-indicators li.active{height:18px;width:18px;border-width:2px;position:relative;box-shadow:0px 0px 0px 1px #808080}.carousel-indicators.active li{background-color:rgba(100,149,253,0.6)}.carousel-indicators.active li.active{background-color:white}.carousel-tablist-highlight{display:block;position:absolute;outline:2px solid transparent;background-color:transparent;box-shadow:0px 0px 0px 1px transparent}.carousel-tablist-highlight.focus{outline:2px solid #6495ED;background-color:rgba(0,0,0,0.4)}a.carousel-control:focus{outline:2px solid #6495ED;background-image:linear-gradient(to right, transparent 0px, rgba(0,0,0,0.5) 100%);box-shadow:0px 0px 0px 1px #000000}.carousel-pause-button{position:absolute;top:-30em;left:-300em;display:block}.carousel-pause-button.focus{top:0.5em;left:0.5em}.carousel:hover .carousel-caption,.carousel.contrast .carousel-caption{background-color:rgba(0,0,0,0.5);z-index:10}.alert-success{color:#2d4821}.alert-info{color:#214c62}.alert-warning{color:#6c4a00;background-color:#f9f1c6}.alert-danger{color:#d2322d}.alert-danger:hover{color:#a82824}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,28 +1,30 @@
|
||||
/*!
|
||||
* Datepicker for Bootstrap v1.6.4 (https://github.com/eternicode/bootstrap-datepicker)
|
||||
*
|
||||
* Copyright 2012 Stefan Petre
|
||||
* Improvements by Andrew Rowls
|
||||
* Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
*/
|
||||
.datepicker {
|
||||
border-radius: 4px;
|
||||
border-radius: 0.25rem;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.datepicker-inline {
|
||||
width: 220px;
|
||||
}
|
||||
.datepicker.datepicker-rtl {
|
||||
|
||||
.datepicker-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
.datepicker.datepicker-rtl table tr td span {
|
||||
|
||||
.datepicker-rtl.dropdown-menu {
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.datepicker-rtl table tr td span {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.datepicker-dropdown {
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.datepicker-dropdown:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
@@ -33,6 +35,7 @@
|
||||
border-bottom-color: rgba(0, 0, 0, 0.2);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.datepicker-dropdown:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
@@ -42,34 +45,43 @@
|
||||
border-top: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.datepicker-dropdown.datepicker-orient-left:before {
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.datepicker-dropdown.datepicker-orient-left:after {
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
.datepicker-dropdown.datepicker-orient-right:before {
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
.datepicker-dropdown.datepicker-orient-right:after {
|
||||
right: 7px;
|
||||
}
|
||||
|
||||
.datepicker-dropdown.datepicker-orient-bottom:before {
|
||||
top: -7px;
|
||||
}
|
||||
|
||||
.datepicker-dropdown.datepicker-orient-bottom:after {
|
||||
top: -6px;
|
||||
}
|
||||
|
||||
.datepicker-dropdown.datepicker-orient-top:before {
|
||||
bottom: -7px;
|
||||
border-bottom: 0;
|
||||
border-top: 7px solid rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.datepicker-dropdown.datepicker-orient-top:after {
|
||||
bottom: -6px;
|
||||
border-bottom: 0;
|
||||
border-top: 6px solid #fff;
|
||||
}
|
||||
|
||||
.datepicker table {
|
||||
margin: 0;
|
||||
-webkit-touch-callout: none;
|
||||
@@ -79,424 +91,325 @@
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.datepicker table tr td,
|
||||
.datepicker table tr th {
|
||||
|
||||
.datepicker table tr td, .datepicker table tr th {
|
||||
text-align: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
.table-striped .datepicker table tr td,
|
||||
.table-striped .datepicker table tr th {
|
||||
|
||||
.table-striped .datepicker table tr td, .table-striped .datepicker table tr th {
|
||||
background-color: transparent;
|
||||
}
|
||||
.datepicker table tr td.old,
|
||||
.datepicker table tr td.new {
|
||||
color: #777777;
|
||||
|
||||
.datepicker table tr td.old, .datepicker table tr td.new {
|
||||
color: #6c757d;
|
||||
}
|
||||
.datepicker table tr td.day:hover,
|
||||
.datepicker table tr td.focused {
|
||||
background: #eeeeee;
|
||||
|
||||
.datepicker table tr td.day:hover, .datepicker table tr td.focused {
|
||||
color: #000;
|
||||
background: #e9e9ea;
|
||||
cursor: pointer;
|
||||
}
|
||||
.datepicker table tr td.disabled,
|
||||
.datepicker table tr td.disabled:hover {
|
||||
|
||||
.datepicker table tr td.disabled, .datepicker table tr td.disabled:hover {
|
||||
background: none;
|
||||
color: #777777;
|
||||
color: #6c757d;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.datepicker table tr td.highlighted {
|
||||
color: #000;
|
||||
background-color: #d9edf7;
|
||||
border-color: #85c5e5;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #83ccd9;
|
||||
border-radius: 0;
|
||||
}
|
||||
.datepicker table tr td.highlighted:focus,
|
||||
.datepicker table tr td.highlighted.focus {
|
||||
|
||||
.datepicker table tr td.highlighted:focus, .datepicker table tr td.highlighted.focus {
|
||||
color: #000;
|
||||
background-color: #afd9ee;
|
||||
border-color: #298fc2;
|
||||
background-color: #bcd4d9;
|
||||
border-color: #6299a3;
|
||||
}
|
||||
|
||||
.datepicker table tr td.highlighted:hover {
|
||||
color: #000;
|
||||
background-color: #afd9ee;
|
||||
border-color: #52addb;
|
||||
background-color: #697679;
|
||||
border-color: #73b3bf;
|
||||
}
|
||||
.datepicker table tr td.highlighted:active,
|
||||
.datepicker table tr td.highlighted.active {
|
||||
|
||||
.datepicker table tr td.highlighted:active, .datepicker table tr td.highlighted.active {
|
||||
color: #000;
|
||||
background-color: #afd9ee;
|
||||
border-color: #52addb;
|
||||
background-color: #bcd4d9;
|
||||
border-color: #73b3bf;
|
||||
}
|
||||
.datepicker table tr td.highlighted:active:hover,
|
||||
.datepicker table tr td.highlighted.active:hover,
|
||||
.datepicker table tr td.highlighted:active:focus,
|
||||
.datepicker table tr td.highlighted.active:focus,
|
||||
.datepicker table tr td.highlighted:active.focus,
|
||||
.datepicker table tr td.highlighted.active.focus {
|
||||
|
||||
.datepicker table tr td.highlighted:active:hover, .datepicker table tr td.highlighted:active:focus, .datepicker table tr td.highlighted.focus:active, .datepicker table tr td.highlighted.active:hover, .datepicker table tr td.highlighted.active:focus, .datepicker table tr td.highlighted.active.focus {
|
||||
color: #000;
|
||||
background-color: #91cbe8;
|
||||
border-color: #298fc2;
|
||||
background-color: #adc4c8;
|
||||
border-color: #6299a3;
|
||||
}
|
||||
.datepicker table tr td.highlighted.disabled:hover,
|
||||
.datepicker table tr td.highlighted[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td.highlighted:hover,
|
||||
.datepicker table tr td.highlighted.disabled:focus,
|
||||
.datepicker table tr td.highlighted[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td.highlighted:focus,
|
||||
.datepicker table tr td.highlighted.disabled.focus,
|
||||
.datepicker table tr td.highlighted[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.highlighted.focus {
|
||||
background-color: #d9edf7;
|
||||
border-color: #85c5e5;
|
||||
|
||||
.datepicker table tr td.highlighted.disabled:hover, .datepicker table tr td.highlighted.disabled:focus, .datepicker table tr td.highlighted.disabled.focus, .datepicker table tr td.highlighted[disabled]:hover, .datepicker table tr td.highlighted[disabled]:focus, .datepicker table tr td.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.highlighted:hover, fieldset[disabled] .datepicker table tr td.highlighted:focus, fieldset[disabled] .datepicker table tr td.highlighted.focus {
|
||||
background-color: #d1ecf1;
|
||||
border-color: #83ccd9;
|
||||
}
|
||||
|
||||
.datepicker table tr td.highlighted.focused {
|
||||
background: #afd9ee;
|
||||
background: #aadce5;
|
||||
}
|
||||
.datepicker table tr td.highlighted.disabled,
|
||||
.datepicker table tr td.highlighted.disabled:active {
|
||||
background: #d9edf7;
|
||||
color: #777777;
|
||||
|
||||
.datepicker table tr td.highlighted.disabled, .datepicker table tr td.highlighted.disabled:active {
|
||||
background: #d1ecf1;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.datepicker table tr td.today {
|
||||
color: #000;
|
||||
background-color: #ffdb99;
|
||||
border-color: #ffb733;
|
||||
}
|
||||
.datepicker table tr td.today:focus,
|
||||
.datepicker table tr td.today.focus {
|
||||
|
||||
.datepicker table tr td.today:focus, .datepicker table tr td.today.focus {
|
||||
color: #000;
|
||||
background-color: #ffc966;
|
||||
border-color: #b37400;
|
||||
background-color: #e6c58a;
|
||||
border-color: #bf8926;
|
||||
}
|
||||
|
||||
.datepicker table tr td.today:hover {
|
||||
color: #000;
|
||||
background-color: #ffc966;
|
||||
border-color: #f59e00;
|
||||
background-color: #806e4d;
|
||||
border-color: #e0a12d;
|
||||
}
|
||||
.datepicker table tr td.today:active,
|
||||
.datepicker table tr td.today.active {
|
||||
|
||||
.datepicker table tr td.today:active, .datepicker table tr td.today.active {
|
||||
color: #000;
|
||||
background-color: #ffc966;
|
||||
border-color: #f59e00;
|
||||
background-color: #e6c58a;
|
||||
border-color: #e0a12d;
|
||||
}
|
||||
.datepicker table tr td.today:active:hover,
|
||||
.datepicker table tr td.today.active:hover,
|
||||
.datepicker table tr td.today:active:focus,
|
||||
.datepicker table tr td.today.active:focus,
|
||||
.datepicker table tr td.today:active.focus,
|
||||
.datepicker table tr td.today.active.focus {
|
||||
|
||||
.datepicker table tr td.today:active:hover, .datepicker table tr td.today:active:focus, .datepicker table tr td.today.focus:active, .datepicker table tr td.today.active:hover, .datepicker table tr td.today.active:focus, .datepicker table tr td.today.active.focus {
|
||||
color: #000;
|
||||
background-color: #ffbc42;
|
||||
border-color: #b37400;
|
||||
background-color: #d4b67f;
|
||||
border-color: #bf8926;
|
||||
}
|
||||
.datepicker table tr td.today.disabled:hover,
|
||||
.datepicker table tr td.today[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td.today:hover,
|
||||
.datepicker table tr td.today.disabled:focus,
|
||||
.datepicker table tr td.today[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td.today:focus,
|
||||
.datepicker table tr td.today.disabled.focus,
|
||||
.datepicker table tr td.today[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.today.focus {
|
||||
|
||||
.datepicker table tr td.today.disabled:hover, .datepicker table tr td.today.disabled:focus, .datepicker table tr td.today.disabled.focus, .datepicker table tr td.today[disabled]:hover, .datepicker table tr td.today[disabled]:focus, .datepicker table tr td.today.focus[disabled], fieldset[disabled] .datepicker table tr td.today:hover, fieldset[disabled] .datepicker table tr td.today:focus, fieldset[disabled] .datepicker table tr td.today.focus {
|
||||
background-color: #ffdb99;
|
||||
border-color: #ffb733;
|
||||
}
|
||||
|
||||
.datepicker table tr td.today.focused {
|
||||
background: #ffc966;
|
||||
}
|
||||
.datepicker table tr td.today.disabled,
|
||||
.datepicker table tr td.today.disabled:active {
|
||||
|
||||
.datepicker table tr td.today.disabled, .datepicker table tr td.today.disabled:active {
|
||||
background: #ffdb99;
|
||||
color: #777777;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range {
|
||||
color: #000;
|
||||
background-color: #eeeeee;
|
||||
border-color: #bbbbbb;
|
||||
background-color: #e9e9ea;
|
||||
border-color: #b5b5b8;
|
||||
border-radius: 0;
|
||||
}
|
||||
.datepicker table tr td.range:focus,
|
||||
.datepicker table tr td.range.focus {
|
||||
|
||||
.datepicker table tr td.range:focus, .datepicker table tr td.range.focus {
|
||||
color: #000;
|
||||
background-color: #d5d5d5;
|
||||
border-color: #7c7c7c;
|
||||
background-color: #d2d2d3;
|
||||
border-color: #88888a;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range:hover {
|
||||
color: #000;
|
||||
background-color: #d5d5d5;
|
||||
border-color: #9d9d9d;
|
||||
background-color: #757575;
|
||||
border-color: #9f9fa2;
|
||||
}
|
||||
.datepicker table tr td.range:active,
|
||||
.datepicker table tr td.range.active {
|
||||
|
||||
.datepicker table tr td.range:active, .datepicker table tr td.range.active {
|
||||
color: #000;
|
||||
background-color: #d5d5d5;
|
||||
border-color: #9d9d9d;
|
||||
background-color: #d2d2d3;
|
||||
border-color: #9f9fa2;
|
||||
}
|
||||
.datepicker table tr td.range:active:hover,
|
||||
.datepicker table tr td.range.active:hover,
|
||||
.datepicker table tr td.range:active:focus,
|
||||
.datepicker table tr td.range.active:focus,
|
||||
.datepicker table tr td.range:active.focus,
|
||||
.datepicker table tr td.range.active.focus {
|
||||
|
||||
.datepicker table tr td.range:active:hover, .datepicker table tr td.range:active:focus, .datepicker table tr td.range.focus:active, .datepicker table tr td.range.active:hover, .datepicker table tr td.range.active:focus, .datepicker table tr td.range.active.focus {
|
||||
color: #000;
|
||||
background-color: #c3c3c3;
|
||||
border-color: #7c7c7c;
|
||||
background-color: #c1c1c2;
|
||||
border-color: #88888a;
|
||||
}
|
||||
.datepicker table tr td.range.disabled:hover,
|
||||
.datepicker table tr td.range[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td.range:hover,
|
||||
.datepicker table tr td.range.disabled:focus,
|
||||
.datepicker table tr td.range[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td.range:focus,
|
||||
.datepicker table tr td.range.disabled.focus,
|
||||
.datepicker table tr td.range[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.focus {
|
||||
background-color: #eeeeee;
|
||||
border-color: #bbbbbb;
|
||||
|
||||
.datepicker table tr td.range.disabled:hover, .datepicker table tr td.range.disabled:focus, .datepicker table tr td.range.disabled.focus, .datepicker table tr td.range[disabled]:hover, .datepicker table tr td.range[disabled]:focus, .datepicker table tr td.range.focus[disabled], fieldset[disabled] .datepicker table tr td.range:hover, fieldset[disabled] .datepicker table tr td.range:focus, fieldset[disabled] .datepicker table tr td.range.focus {
|
||||
background-color: #e9e9ea;
|
||||
border-color: #b5b5b8;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.focused {
|
||||
background: #d5d5d5;
|
||||
background: #cfcfd1;
|
||||
}
|
||||
.datepicker table tr td.range.disabled,
|
||||
.datepicker table tr td.range.disabled:active {
|
||||
background: #eeeeee;
|
||||
color: #777777;
|
||||
|
||||
.datepicker table tr td.range.disabled, .datepicker table tr td.range.disabled:active {
|
||||
background: #e9e9ea;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.highlighted {
|
||||
color: #000;
|
||||
background-color: #e4eef3;
|
||||
border-color: #9dc1d3;
|
||||
background-color: #ddebee;
|
||||
border-color: #99c3cc;
|
||||
}
|
||||
.datepicker table tr td.range.highlighted:focus,
|
||||
.datepicker table tr td.range.highlighted.focus {
|
||||
|
||||
.datepicker table tr td.range.highlighted:focus, .datepicker table tr td.range.highlighted.focus {
|
||||
color: #000;
|
||||
background-color: #c1d7e3;
|
||||
border-color: #4b88a6;
|
||||
background-color: #c7d4d6;
|
||||
border-color: #739299;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.highlighted:hover {
|
||||
color: #000;
|
||||
background-color: #c1d7e3;
|
||||
border-color: #73a6c0;
|
||||
background-color: #6f7677;
|
||||
border-color: #87acb4;
|
||||
}
|
||||
.datepicker table tr td.range.highlighted:active,
|
||||
.datepicker table tr td.range.highlighted.active {
|
||||
|
||||
.datepicker table tr td.range.highlighted:active, .datepicker table tr td.range.highlighted.active {
|
||||
color: #000;
|
||||
background-color: #c1d7e3;
|
||||
border-color: #73a6c0;
|
||||
background-color: #c7d4d6;
|
||||
border-color: #87acb4;
|
||||
}
|
||||
.datepicker table tr td.range.highlighted:active:hover,
|
||||
.datepicker table tr td.range.highlighted.active:hover,
|
||||
.datepicker table tr td.range.highlighted:active:focus,
|
||||
.datepicker table tr td.range.highlighted.active:focus,
|
||||
.datepicker table tr td.range.highlighted:active.focus,
|
||||
.datepicker table tr td.range.highlighted.active.focus {
|
||||
|
||||
.datepicker table tr td.range.highlighted:active:hover, .datepicker table tr td.range.highlighted:active:focus, .datepicker table tr td.range.highlighted.focus:active, .datepicker table tr td.range.highlighted.active:hover, .datepicker table tr td.range.highlighted.active:focus, .datepicker table tr td.range.highlighted.active.focus {
|
||||
color: #000;
|
||||
background-color: #a8c8d8;
|
||||
border-color: #4b88a6;
|
||||
background-color: #b7c3c6;
|
||||
border-color: #739299;
|
||||
}
|
||||
.datepicker table tr td.range.highlighted.disabled:hover,
|
||||
.datepicker table tr td.range.highlighted[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td.range.highlighted:hover,
|
||||
.datepicker table tr td.range.highlighted.disabled:focus,
|
||||
.datepicker table tr td.range.highlighted[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.highlighted:focus,
|
||||
.datepicker table tr td.range.highlighted.disabled.focus,
|
||||
.datepicker table tr td.range.highlighted[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.highlighted.focus {
|
||||
background-color: #e4eef3;
|
||||
border-color: #9dc1d3;
|
||||
|
||||
.datepicker table tr td.range.highlighted.disabled:hover, .datepicker table tr td.range.highlighted.disabled:focus, .datepicker table tr td.range.highlighted.disabled.focus, .datepicker table tr td.range.highlighted[disabled]:hover, .datepicker table tr td.range.highlighted[disabled]:focus, .datepicker table tr td.range.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.range.highlighted:hover, fieldset[disabled] .datepicker table tr td.range.highlighted:focus, fieldset[disabled] .datepicker table tr td.range.highlighted.focus {
|
||||
background-color: #ddebee;
|
||||
border-color: #99c3cc;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.highlighted.focused {
|
||||
background: #c1d7e3;
|
||||
background: #bbd7dd;
|
||||
}
|
||||
.datepicker table tr td.range.highlighted.disabled,
|
||||
.datepicker table tr td.range.highlighted.disabled:active {
|
||||
background: #e4eef3;
|
||||
color: #777777;
|
||||
|
||||
.datepicker table tr td.range.highlighted.disabled, .datepicker table tr td.range.highlighted.disabled:active {
|
||||
background: #ddebee;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.today {
|
||||
color: #000;
|
||||
background-color: #f7ca77;
|
||||
border-color: #f1a417;
|
||||
background-color: #f4c775;
|
||||
border-color: #eca117;
|
||||
}
|
||||
.datepicker table tr td.range.today:focus,
|
||||
.datepicker table tr td.range.today.focus {
|
||||
|
||||
.datepicker table tr td.range.today:focus, .datepicker table tr td.range.today.focus {
|
||||
color: #000;
|
||||
background-color: #f4b747;
|
||||
border-color: #815608;
|
||||
background-color: #dcb369;
|
||||
border-color: #b17811;
|
||||
}
|
||||
|
||||
.datepicker table tr td.range.today:hover {
|
||||
color: #000;
|
||||
background-color: #f4b747;
|
||||
border-color: #bf800c;
|
||||
background-color: #7a643b;
|
||||
border-color: #d08d14;
|
||||
}
|
||||
.datepicker table tr td.range.today:active,
|
||||
.datepicker table tr td.range.today.active {
|
||||
|
||||
.datepicker table tr td.range.today:active, .datepicker table tr td.range.today.active {
|
||||
color: #000;
|
||||
background-color: #f4b747;
|
||||
border-color: #bf800c;
|
||||
background-color: #dcb369;
|
||||
border-color: #d08d14;
|
||||
}
|
||||
.datepicker table tr td.range.today:active:hover,
|
||||
.datepicker table tr td.range.today.active:hover,
|
||||
.datepicker table tr td.range.today:active:focus,
|
||||
.datepicker table tr td.range.today.active:focus,
|
||||
.datepicker table tr td.range.today:active.focus,
|
||||
.datepicker table tr td.range.today.active.focus {
|
||||
|
||||
.datepicker table tr td.range.today:active:hover, .datepicker table tr td.range.today:active:focus, .datepicker table tr td.range.today.focus:active, .datepicker table tr td.range.today.active:hover, .datepicker table tr td.range.today.active:focus, .datepicker table tr td.range.today.active.focus {
|
||||
color: #000;
|
||||
background-color: #f2aa25;
|
||||
border-color: #815608;
|
||||
background-color: #cba561;
|
||||
border-color: #b17811;
|
||||
}
|
||||
.datepicker table tr td.range.today.disabled:hover,
|
||||
.datepicker table tr td.range.today[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td.range.today:hover,
|
||||
.datepicker table tr td.range.today.disabled:focus,
|
||||
.datepicker table tr td.range.today[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.today:focus,
|
||||
.datepicker table tr td.range.today.disabled.focus,
|
||||
.datepicker table tr td.range.today[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.focus {
|
||||
background-color: #f7ca77;
|
||||
border-color: #f1a417;
|
||||
|
||||
.datepicker table tr td.range.today.disabled:hover, .datepicker table tr td.range.today.disabled:focus, .datepicker table tr td.range.today.disabled.focus, .datepicker table tr td.range.today[disabled]:hover, .datepicker table tr td.range.today[disabled]:focus, .datepicker table tr td.range.today.focus[disabled], fieldset[disabled] .datepicker table tr td.range.today:hover, fieldset[disabled] .datepicker table tr td.range.today:focus, fieldset[disabled] .datepicker table tr td.range.today.focus {
|
||||
background-color: #f4c775;
|
||||
border-color: #eca117;
|
||||
}
|
||||
.datepicker table tr td.range.today.disabled,
|
||||
.datepicker table tr td.range.today.disabled:active {
|
||||
background: #f7ca77;
|
||||
color: #777777;
|
||||
|
||||
.datepicker table tr td.range.today.disabled, .datepicker table tr td.range.today.disabled:active {
|
||||
background: #f4c775;
|
||||
color: #6c757d;
|
||||
}
|
||||
.datepicker table tr td.selected,
|
||||
.datepicker table tr td.selected.highlighted {
|
||||
|
||||
.datepicker table tr td.selected, .datepicker table tr td.selected.highlighted {
|
||||
color: #fff;
|
||||
background-color: #777777;
|
||||
border-color: #555555;
|
||||
background-color: #898b8d;
|
||||
border-color: #6b6e71;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.datepicker table tr td.selected:focus,
|
||||
.datepicker table tr td.selected.highlighted:focus,
|
||||
.datepicker table tr td.selected.focus,
|
||||
.datepicker table tr td.selected.highlighted.focus {
|
||||
|
||||
.datepicker table tr td.selected:focus, .datepicker table tr td.selected.focus, .datepicker table tr td.selected.highlighted:focus, .datepicker table tr td.selected.highlighted.focus {
|
||||
color: #fff;
|
||||
background-color: #5e5e5e;
|
||||
border-color: #161616;
|
||||
background-color: #959798;
|
||||
border-color: #909295;
|
||||
}
|
||||
.datepicker table tr td.selected:hover,
|
||||
.datepicker table tr td.selected.highlighted:hover {
|
||||
|
||||
.datepicker table tr td.selected:hover, .datepicker table tr td.selected.highlighted:hover {
|
||||
color: #fff;
|
||||
background-color: #5e5e5e;
|
||||
border-color: #373737;
|
||||
background-color: #c4c5c6;
|
||||
border-color: #7d7f82;
|
||||
}
|
||||
.datepicker table tr td.selected:active,
|
||||
.datepicker table tr td.selected.highlighted:active,
|
||||
.datepicker table tr td.selected.active,
|
||||
.datepicker table tr td.selected.highlighted.active {
|
||||
|
||||
.datepicker table tr td.selected:active, .datepicker table tr td.selected.active, .datepicker table tr td.selected.highlighted:active, .datepicker table tr td.selected.highlighted.active {
|
||||
color: #fff;
|
||||
background-color: #5e5e5e;
|
||||
border-color: #373737;
|
||||
background-color: #959798;
|
||||
border-color: #7d7f82;
|
||||
}
|
||||
.datepicker table tr td.selected:active:hover,
|
||||
.datepicker table tr td.selected.highlighted:active:hover,
|
||||
.datepicker table tr td.selected.active:hover,
|
||||
.datepicker table tr td.selected.highlighted.active:hover,
|
||||
.datepicker table tr td.selected:active:focus,
|
||||
.datepicker table tr td.selected.highlighted:active:focus,
|
||||
.datepicker table tr td.selected.active:focus,
|
||||
.datepicker table tr td.selected.highlighted.active:focus,
|
||||
.datepicker table tr td.selected:active.focus,
|
||||
.datepicker table tr td.selected.highlighted:active.focus,
|
||||
.datepicker table tr td.selected.active.focus,
|
||||
.datepicker table tr td.selected.highlighted.active.focus {
|
||||
|
||||
.datepicker table tr td.selected:active:hover, .datepicker table tr td.selected:active:focus, .datepicker table tr td.selected.focus:active, .datepicker table tr td.selected.active:hover, .datepicker table tr td.selected.active:focus, .datepicker table tr td.selected.active.focus, .datepicker table tr td.selected.highlighted:active:hover, .datepicker table tr td.selected.highlighted:active:focus, .datepicker table tr td.selected.highlighted.focus:active, .datepicker table tr td.selected.highlighted.active:hover, .datepicker table tr td.selected.highlighted.active:focus, .datepicker table tr td.selected.highlighted.active.focus {
|
||||
color: #fff;
|
||||
background-color: #4c4c4c;
|
||||
border-color: #161616;
|
||||
background-color: #9d9fa0;
|
||||
border-color: #909295;
|
||||
}
|
||||
.datepicker table tr td.selected.disabled:hover,
|
||||
.datepicker table tr td.selected.highlighted.disabled:hover,
|
||||
.datepicker table tr td.selected[disabled]:hover,
|
||||
.datepicker table tr td.selected.highlighted[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td.selected:hover,
|
||||
fieldset[disabled] .datepicker table tr td.selected.highlighted:hover,
|
||||
.datepicker table tr td.selected.disabled:focus,
|
||||
.datepicker table tr td.selected.highlighted.disabled:focus,
|
||||
.datepicker table tr td.selected[disabled]:focus,
|
||||
.datepicker table tr td.selected.highlighted[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td.selected:focus,
|
||||
fieldset[disabled] .datepicker table tr td.selected.highlighted:focus,
|
||||
.datepicker table tr td.selected.disabled.focus,
|
||||
.datepicker table tr td.selected.highlighted.disabled.focus,
|
||||
.datepicker table tr td.selected[disabled].focus,
|
||||
.datepicker table tr td.selected.highlighted[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.selected.focus,
|
||||
fieldset[disabled] .datepicker table tr td.selected.highlighted.focus {
|
||||
background-color: #777777;
|
||||
border-color: #555555;
|
||||
|
||||
.datepicker table tr td.selected.disabled:hover, .datepicker table tr td.selected.disabled:focus, .datepicker table tr td.selected.disabled.focus, .datepicker table tr td.selected[disabled]:hover, .datepicker table tr td.selected[disabled]:focus, .datepicker table tr td.selected.focus[disabled], fieldset[disabled] .datepicker table tr td.selected:hover, fieldset[disabled] .datepicker table tr td.selected:focus, fieldset[disabled] .datepicker table tr td.selected.focus, .datepicker table tr td.selected.highlighted.disabled:hover, .datepicker table tr td.selected.highlighted.disabled:focus, .datepicker table tr td.selected.highlighted.disabled.focus, .datepicker table tr td.selected.highlighted[disabled]:hover, .datepicker table tr td.selected.highlighted[disabled]:focus, .datepicker table tr td.selected.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.selected.highlighted:hover, fieldset[disabled] .datepicker table tr td.selected.highlighted:focus, fieldset[disabled] .datepicker table tr td.selected.highlighted.focus {
|
||||
background-color: #898b8d;
|
||||
border-color: #6b6e71;
|
||||
}
|
||||
.datepicker table tr td.active,
|
||||
.datepicker table tr td.active.highlighted {
|
||||
|
||||
.datepicker table tr td.active, .datepicker table tr td.active.highlighted {
|
||||
color: #fff;
|
||||
background-color: #337ab7;
|
||||
border-color: #2e6da4;
|
||||
background-color: #007bff;
|
||||
border-color: #0277f4;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.datepicker table tr td.active:focus,
|
||||
.datepicker table tr td.active.highlighted:focus,
|
||||
.datepicker table tr td.active.focus,
|
||||
.datepicker table tr td.active.highlighted.focus {
|
||||
|
||||
.datepicker table tr td.active:focus, .datepicker table tr td.active.focus, .datepicker table tr td.active.highlighted:focus, .datepicker table tr td.active.highlighted.focus {
|
||||
color: #fff;
|
||||
background-color: #286090;
|
||||
border-color: #122b40;
|
||||
background-color: #1a88ff;
|
||||
border-color: #4199f7;
|
||||
}
|
||||
.datepicker table tr td.active:hover,
|
||||
.datepicker table tr td.active.highlighted:hover {
|
||||
|
||||
.datepicker table tr td.active:hover, .datepicker table tr td.active.highlighted:hover {
|
||||
color: #fff;
|
||||
background-color: #286090;
|
||||
border-color: #204d74;
|
||||
background-color: #80bdff;
|
||||
border-color: #2087f5;
|
||||
}
|
||||
.datepicker table tr td.active:active,
|
||||
.datepicker table tr td.active.highlighted:active,
|
||||
.datepicker table tr td.active.active,
|
||||
.datepicker table tr td.active.highlighted.active {
|
||||
|
||||
.datepicker table tr td.active:active, .datepicker table tr td.active.active, .datepicker table tr td.active.highlighted:active, .datepicker table tr td.active.highlighted.active {
|
||||
color: #fff;
|
||||
background-color: #286090;
|
||||
border-color: #204d74;
|
||||
background-color: #1a88ff;
|
||||
border-color: #2087f5;
|
||||
}
|
||||
.datepicker table tr td.active:active:hover,
|
||||
.datepicker table tr td.active.highlighted:active:hover,
|
||||
.datepicker table tr td.active.active:hover,
|
||||
.datepicker table tr td.active.highlighted.active:hover,
|
||||
.datepicker table tr td.active:active:focus,
|
||||
.datepicker table tr td.active.highlighted:active:focus,
|
||||
.datepicker table tr td.active.active:focus,
|
||||
.datepicker table tr td.active.highlighted.active:focus,
|
||||
.datepicker table tr td.active:active.focus,
|
||||
.datepicker table tr td.active.highlighted:active.focus,
|
||||
.datepicker table tr td.active.active.focus,
|
||||
.datepicker table tr td.active.highlighted.active.focus {
|
||||
|
||||
.datepicker table tr td.active:active:hover, .datepicker table tr td.active:active:focus, .datepicker table tr td.active.focus:active, .datepicker table tr td.active.active:hover, .datepicker table tr td.active.active:focus, .datepicker table tr td.active.active.focus, .datepicker table tr td.active.highlighted:active:hover, .datepicker table tr td.active.highlighted:active:focus, .datepicker table tr td.active.highlighted.focus:active, .datepicker table tr td.active.highlighted.active:hover, .datepicker table tr td.active.highlighted.active:focus, .datepicker table tr td.active.highlighted.active.focus {
|
||||
color: #fff;
|
||||
background-color: #204d74;
|
||||
border-color: #122b40;
|
||||
background-color: #2b91ff;
|
||||
border-color: #4199f7;
|
||||
}
|
||||
.datepicker table tr td.active.disabled:hover,
|
||||
.datepicker table tr td.active.highlighted.disabled:hover,
|
||||
.datepicker table tr td.active[disabled]:hover,
|
||||
.datepicker table tr td.active.highlighted[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td.active:hover,
|
||||
fieldset[disabled] .datepicker table tr td.active.highlighted:hover,
|
||||
.datepicker table tr td.active.disabled:focus,
|
||||
.datepicker table tr td.active.highlighted.disabled:focus,
|
||||
.datepicker table tr td.active[disabled]:focus,
|
||||
.datepicker table tr td.active.highlighted[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td.active:focus,
|
||||
fieldset[disabled] .datepicker table tr td.active.highlighted:focus,
|
||||
.datepicker table tr td.active.disabled.focus,
|
||||
.datepicker table tr td.active.highlighted.disabled.focus,
|
||||
.datepicker table tr td.active[disabled].focus,
|
||||
.datepicker table tr td.active.highlighted[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td.active.focus,
|
||||
fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
|
||||
background-color: #337ab7;
|
||||
border-color: #2e6da4;
|
||||
|
||||
.datepicker table tr td.active.disabled:hover, .datepicker table tr td.active.disabled:focus, .datepicker table tr td.active.disabled.focus, .datepicker table tr td.active[disabled]:hover, .datepicker table tr td.active[disabled]:focus, .datepicker table tr td.active.focus[disabled], fieldset[disabled] .datepicker table tr td.active:hover, fieldset[disabled] .datepicker table tr td.active:focus, fieldset[disabled] .datepicker table tr td.active.focus, .datepicker table tr td.active.highlighted.disabled:hover, .datepicker table tr td.active.highlighted.disabled:focus, .datepicker table tr td.active.highlighted.disabled.focus, .datepicker table tr td.active.highlighted[disabled]:hover, .datepicker table tr td.active.highlighted[disabled]:focus, .datepicker table tr td.active.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.active.highlighted:hover, fieldset[disabled] .datepicker table tr td.active.highlighted:focus, fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
|
||||
background-color: #007bff;
|
||||
border-color: #0277f4;
|
||||
}
|
||||
|
||||
.datepicker table tr td span {
|
||||
display: block;
|
||||
width: 23%;
|
||||
@@ -507,172 +420,114 @@ fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.datepicker table tr td span:hover,
|
||||
.datepicker table tr td span.focused {
|
||||
background: #eeeeee;
|
||||
|
||||
.datepicker table tr td span:hover, .datepicker table tr td span.focused {
|
||||
color: #000;
|
||||
background: #e9e9ea;
|
||||
}
|
||||
.datepicker table tr td span.disabled,
|
||||
.datepicker table tr td span.disabled:hover {
|
||||
|
||||
.datepicker table tr td span.disabled, .datepicker table tr td span.disabled:hover {
|
||||
background: none;
|
||||
color: #777777;
|
||||
color: #6c757d;
|
||||
cursor: default;
|
||||
}
|
||||
.datepicker table tr td span.active,
|
||||
.datepicker table tr td span.active:hover,
|
||||
.datepicker table tr td span.active.disabled,
|
||||
.datepicker table tr td span.active.disabled:hover {
|
||||
|
||||
.datepicker table tr td span.active, .datepicker table tr td span.active:hover, .datepicker table tr td span.active.disabled, .datepicker table tr td span.active.disabled:hover {
|
||||
color: #fff;
|
||||
background-color: #337ab7;
|
||||
border-color: #2e6da4;
|
||||
background-color: #007bff;
|
||||
border-color: #0277f4;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.datepicker table tr td span.active:focus,
|
||||
.datepicker table tr td span.active:hover:focus,
|
||||
.datepicker table tr td span.active.disabled:focus,
|
||||
.datepicker table tr td span.active.disabled:hover:focus,
|
||||
.datepicker table tr td span.active.focus,
|
||||
.datepicker table tr td span.active:hover.focus,
|
||||
.datepicker table tr td span.active.disabled.focus,
|
||||
.datepicker table tr td span.active.disabled:hover.focus {
|
||||
|
||||
.datepicker table tr td span.active:focus, .datepicker table tr td span.active.focus, .datepicker table tr td span.active:hover:focus, .datepicker table tr td span.active.focus:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled.focus:hover {
|
||||
color: #fff;
|
||||
background-color: #286090;
|
||||
border-color: #122b40;
|
||||
background-color: #1a88ff;
|
||||
border-color: #4199f7;
|
||||
}
|
||||
.datepicker table tr td span.active:hover,
|
||||
.datepicker table tr td span.active:hover:hover,
|
||||
.datepicker table tr td span.active.disabled:hover,
|
||||
.datepicker table tr td span.active.disabled:hover:hover {
|
||||
|
||||
.datepicker table tr td span.active:hover, .datepicker table tr td span.active:hover:hover, .datepicker table tr td span.active.disabled:hover, .datepicker table tr td span.active.disabled:hover:hover {
|
||||
color: #fff;
|
||||
background-color: #286090;
|
||||
border-color: #204d74;
|
||||
background-color: #80bdff;
|
||||
border-color: #2087f5;
|
||||
}
|
||||
.datepicker table tr td span.active:active,
|
||||
.datepicker table tr td span.active:hover:active,
|
||||
.datepicker table tr td span.active.disabled:active,
|
||||
.datepicker table tr td span.active.disabled:hover:active,
|
||||
.datepicker table tr td span.active.active,
|
||||
.datepicker table tr td span.active:hover.active,
|
||||
.datepicker table tr td span.active.disabled.active,
|
||||
.datepicker table tr td span.active.disabled:hover.active {
|
||||
|
||||
.datepicker table tr td span.active:active, .datepicker table tr td span.active.active, .datepicker table tr td span.active:hover:active, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.disabled:active, .datepicker table tr td span.active.disabled.active, .datepicker table tr td span.active.disabled:hover:active, .datepicker table tr td span.active.disabled.active:hover {
|
||||
color: #fff;
|
||||
background-color: #286090;
|
||||
border-color: #204d74;
|
||||
background-color: #1a88ff;
|
||||
border-color: #2087f5;
|
||||
}
|
||||
.datepicker table tr td span.active:active:hover,
|
||||
.datepicker table tr td span.active:hover:active:hover,
|
||||
.datepicker table tr td span.active.disabled:active:hover,
|
||||
.datepicker table tr td span.active.disabled:hover:active:hover,
|
||||
.datepicker table tr td span.active.active:hover,
|
||||
.datepicker table tr td span.active:hover.active:hover,
|
||||
.datepicker table tr td span.active.disabled.active:hover,
|
||||
.datepicker table tr td span.active.disabled:hover.active:hover,
|
||||
.datepicker table tr td span.active:active:focus,
|
||||
.datepicker table tr td span.active:hover:active:focus,
|
||||
.datepicker table tr td span.active.disabled:active:focus,
|
||||
.datepicker table tr td span.active.disabled:hover:active:focus,
|
||||
.datepicker table tr td span.active.active:focus,
|
||||
.datepicker table tr td span.active:hover.active:focus,
|
||||
.datepicker table tr td span.active.disabled.active:focus,
|
||||
.datepicker table tr td span.active.disabled:hover.active:focus,
|
||||
.datepicker table tr td span.active:active.focus,
|
||||
.datepicker table tr td span.active:hover:active.focus,
|
||||
.datepicker table tr td span.active.disabled:active.focus,
|
||||
.datepicker table tr td span.active.disabled:hover:active.focus,
|
||||
.datepicker table tr td span.active.active.focus,
|
||||
.datepicker table tr td span.active:hover.active.focus,
|
||||
.datepicker table tr td span.active.disabled.active.focus,
|
||||
.datepicker table tr td span.active.disabled:hover.active.focus {
|
||||
|
||||
.datepicker table tr td span.active:active:hover, .datepicker table tr td span.active:active:focus, .datepicker table tr td span.active.focus:active, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.active:focus, .datepicker table tr td span.active.active.focus, .datepicker table tr td span.active:hover:active:hover, .datepicker table tr td span.active:hover:active:focus, .datepicker table tr td span.active.focus:hover:active, .datepicker table tr td span.active.active:hover:hover, .datepicker table tr td span.active.active:hover:focus, .datepicker table tr td span.active.active.focus:hover, .datepicker table tr td span.active.disabled:active:hover, .datepicker table tr td span.active.disabled:active:focus, .datepicker table tr td span.active.disabled.focus:active, .datepicker table tr td span.active.disabled.active:hover, .datepicker table tr td span.active.disabled.active:focus, .datepicker table tr td span.active.disabled.active.focus, .datepicker table tr td span.active.disabled:hover:active:hover, .datepicker table tr td span.active.disabled:hover:active:focus, .datepicker table tr td span.active.disabled.focus:hover:active, .datepicker table tr td span.active.disabled.active:hover:hover, .datepicker table tr td span.active.disabled.active:hover:focus, .datepicker table tr td span.active.disabled.active.focus:hover {
|
||||
color: #fff;
|
||||
background-color: #204d74;
|
||||
border-color: #122b40;
|
||||
background-color: #2b91ff;
|
||||
border-color: #4199f7;
|
||||
}
|
||||
.datepicker table tr td span.active.disabled:hover,
|
||||
.datepicker table tr td span.active:hover.disabled:hover,
|
||||
.datepicker table tr td span.active.disabled.disabled:hover,
|
||||
.datepicker table tr td span.active.disabled:hover.disabled:hover,
|
||||
.datepicker table tr td span.active[disabled]:hover,
|
||||
.datepicker table tr td span.active:hover[disabled]:hover,
|
||||
.datepicker table tr td span.active.disabled[disabled]:hover,
|
||||
.datepicker table tr td span.active.disabled:hover[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover,
|
||||
.datepicker table tr td span.active.disabled:focus,
|
||||
.datepicker table tr td span.active:hover.disabled:focus,
|
||||
.datepicker table tr td span.active.disabled.disabled:focus,
|
||||
.datepicker table tr td span.active.disabled:hover.disabled:focus,
|
||||
.datepicker table tr td span.active[disabled]:focus,
|
||||
.datepicker table tr td span.active:hover[disabled]:focus,
|
||||
.datepicker table tr td span.active.disabled[disabled]:focus,
|
||||
.datepicker table tr td span.active.disabled:hover[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active:focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover:focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus,
|
||||
.datepicker table tr td span.active.disabled.focus,
|
||||
.datepicker table tr td span.active:hover.disabled.focus,
|
||||
.datepicker table tr td span.active.disabled.disabled.focus,
|
||||
.datepicker table tr td span.active.disabled:hover.disabled.focus,
|
||||
.datepicker table tr td span.active[disabled].focus,
|
||||
.datepicker table tr td span.active:hover[disabled].focus,
|
||||
.datepicker table tr td span.active.disabled[disabled].focus,
|
||||
.datepicker table tr td span.active.disabled:hover[disabled].focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active.focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover.focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled.focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover.focus {
|
||||
background-color: #337ab7;
|
||||
border-color: #2e6da4;
|
||||
|
||||
.datepicker table tr td span.active.disabled:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active[disabled]:hover, .datepicker table tr td span.active[disabled]:focus, .datepicker table tr td span.active.focus[disabled], fieldset[disabled] .datepicker table tr td span.active:hover, fieldset[disabled] .datepicker table tr td span.active:focus, fieldset[disabled] .datepicker table tr td span.active.focus, .datepicker table tr td span.active.disabled:hover:hover, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled.focus:hover, .datepicker table tr td span.active[disabled]:hover:hover, .datepicker table tr td span.active[disabled]:hover:focus, .datepicker table tr td span.active.focus[disabled]:hover, fieldset[disabled] .datepicker table tr td span.active:hover:hover, fieldset[disabled] .datepicker table tr td span.active:hover:focus, fieldset[disabled] .datepicker table tr td span.active.focus:hover, .datepicker table tr td span.active.disabled.disabled:hover, .datepicker table tr td span.active.disabled.disabled:focus, .datepicker table tr td span.active.disabled.disabled.focus, .datepicker table tr td span.active.disabled[disabled]:hover, .datepicker table tr td span.active.disabled[disabled]:focus, .datepicker table tr td span.active.disabled.focus[disabled], fieldset[disabled] .datepicker table tr td span.active.disabled:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:focus, fieldset[disabled] .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled.disabled:hover:hover, .datepicker table tr td span.active.disabled.disabled:hover:focus, .datepicker table tr td span.active.disabled.disabled.focus:hover, .datepicker table tr td span.active.disabled[disabled]:hover:hover, .datepicker table tr td span.active.disabled[disabled]:hover:focus, .datepicker table tr td span.active.disabled.focus[disabled]:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus, fieldset[disabled] .datepicker table tr td span.active.disabled.focus:hover {
|
||||
background-color: #007bff;
|
||||
border-color: #0277f4;
|
||||
}
|
||||
.datepicker table tr td span.old,
|
||||
.datepicker table tr td span.new {
|
||||
color: #777777;
|
||||
|
||||
.datepicker table tr td span.old, .datepicker table tr td span.new {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.datepicker .datepicker-switch {
|
||||
width: 145px;
|
||||
}
|
||||
|
||||
.datepicker .datepicker-switch,
|
||||
.datepicker .prev,
|
||||
.datepicker .next,
|
||||
.datepicker tfoot tr th {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.datepicker .datepicker-switch:hover,
|
||||
.datepicker .prev:hover,
|
||||
.datepicker .next:hover,
|
||||
.datepicker tfoot tr th:hover {
|
||||
background: #eeeeee;
|
||||
color: #000;
|
||||
background: #e9e9ea;
|
||||
}
|
||||
|
||||
.datepicker .prev.disabled, .datepicker .next.disabled {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.datepicker .cw {
|
||||
font-size: 10px;
|
||||
width: 12px;
|
||||
padding: 0 2px 0 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.input-group.date .input-group-addon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-daterange {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-daterange input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-daterange input:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.input-daterange input:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.input-daterange .input-group-addon {
|
||||
width: auto;
|
||||
min-width: 16px;
|
||||
padding: 4px 5px;
|
||||
line-height: 1.42857143;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
line-height: 1.5;
|
||||
border-width: 1px 0;
|
||||
margin-left: -5px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-datepicker3.css.map */
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker-en-CA.min.js
vendored
Normal file
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker-en-CA.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates["en-CA"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:0,format:"yyyy-mm-dd"},a.fn.datepicker.deprecated("This filename doesn't follow the convention, use bootstrap-datepicker.en-CA.js instead.")}(jQuery);
|
||||
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ar-tn.min.js
vendored
Normal file
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.ar-tn.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates["ar-tn"]={days:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت","الأحد"],daysShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت","أحد"],daysMin:["ح","ن","ث","ع","خ","ج","س","ح"],months:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthsShort:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],today:"هذا اليوم",rtl:!0}}(jQuery);
|
||||
@@ -1 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.az={days:["Bazar","Bazar ertəsi","Çərşənbə axşamı","Çərşənbə","Cümə axşamı","Cümə","Şənbə"],daysShort:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],daysMin:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],months:["Yanvar","Fevral","Mart","Aprel","May","İyun","İyul","Avqust","Sentyabr","Oktyabr","Noyabr","Dekabr"],monthsShort:["Yan","Fev","Mar","Apr","May","İyun","İyul","Avq","Sen","Okt","Noy","Dek"],today:"Bu gün",weekStart:1}}(jQuery);
|
||||
!function(a){a.fn.datepicker.dates.az={days:["Bazar","Bazar ertəsi","Çərşənbə axşamı","Çərşənbə","Cümə axşamı","Cümə","Şənbə"],daysShort:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],daysMin:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],months:["Yanvar","Fevral","Mart","Aprel","May","İyun","İyul","Avqust","Sentyabr","Oktyabr","Noyabr","Dekabr"],monthsShort:["Yan","Fev","Mar","Apr","May","İyun","İyul","Avq","Sen","Okt","Noy","Dek"],today:"Bu gün",weekStart:1,clear:"Təmizlə",monthsTitle:"Aylar"}}(jQuery);
|
||||
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.bm.min.js
vendored
Normal file
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.bm.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.bm={days:["Kari","Ntɛnɛn","Tarata","Araba","Alamisa","Juma","Sibiri"],daysShort:["Kar","Ntɛ","Tar","Ara","Ala","Jum","Sib"],daysMin:["Ka","Nt","Ta","Ar","Al","Ju","Si"],months:["Zanwuyekalo","Fewuruyekalo","Marisikalo","Awirilikalo","Mɛkalo","Zuwɛnkalo","Zuluyekalo","Utikalo","Sɛtanburukalo","ɔkutɔburukalo","Nowanburukalo","Desanburukalo"],monthsShort:["Zan","Few","Mar","Awi","Mɛ","Zuw","Zul","Uti","Sɛt","ɔku","Now","Des"],today:"Bi",monthsTitle:"Kalo",clear:"Ka jɔsi",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);
|
||||
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.bn.min.js
vendored
Normal file
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.bn.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.bn={days:["রবিবার","সোমবার","মঙ্গলবার","বুধবার","বৃহস্পতিবার","শুক্রবার","শনিবার"],daysShort:["রবিবার","সোমবার","মঙ্গলবার","বুধবার","বৃহস্পতিবার","শুক্রবার","শনিবার"],daysMin:["রবি","সোম","মঙ্গল","বুধ","বৃহস্পতি","শুক্র","শনি"],months:["জানুয়ারী","ফেব্রুয়ারি","মার্চ","এপ্রিল","মে","জুন","জুলাই","অগাস্ট","সেপ্টেম্বর","অক্টোবর","নভেম্বর","ডিসেম্বর"],monthsShort:["জানুয়ারী","ফেব্রুয়ারি","মার্চ","এপ্রিল","মে","জুন","জুলাই","অগাস্ট","সেপ্টেম্বর","অক্টোবর","নভেম্বর","ডিসেম্বর"],today:"আজ",monthsTitle:"মাস",clear:"পরিষ্কার",weekStart:0,format:"mm/dd/yyyy"}}(jQuery);
|
||||
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.br.min.js
vendored
Normal file
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.br.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.br={days:["Sul","Lun","Meurzh","Merc'her","Yaou","Gwener","Sadorn"],daysShort:["Sul","Lun","Meu.","Mer.","Yao.","Gwe.","Sad."],daysMin:["Su","L","Meu","Mer","Y","G","Sa"],months:["Genver","C'hwevrer","Meurzh","Ebrel","Mae","Mezheven","Gouere","Eost","Gwengolo","Here","Du","Kerzu"],monthsShort:["Genv.","C'hw.","Meur.","Ebre.","Mae","Mezh.","Goue.","Eost","Gwen.","Here","Du","Kerz."],today:"Hiziv",monthsTitle:"Miz",clear:"Dilemel",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);
|
||||
@@ -1 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.cs={days:["Neděle","Pondělí","Úterý","Středa","Čtvrtek","Pátek","Sobota"],daysShort:["Ned","Pon","Úte","Stř","Čtv","Pát","Sob"],daysMin:["Ne","Po","Út","St","Čt","Pá","So"],months:["Leden","Únor","Březen","Duben","Květen","Červen","Červenec","Srpen","Září","Říjen","Listopad","Prosinec"],monthsShort:["Led","Úno","Bře","Dub","Kvě","Čer","Čnc","Srp","Zář","Říj","Lis","Pro"],today:"Dnes",clear:"Vymazat",weekStart:1,format:"dd.m.yyyy"}}(jQuery);
|
||||
!function(a){a.fn.datepicker.dates.cs={days:["Neděle","Pondělí","Úterý","Středa","Čtvrtek","Pátek","Sobota"],daysShort:["Ned","Pon","Úte","Stř","Čtv","Pát","Sob"],daysMin:["Ne","Po","Út","St","Čt","Pá","So"],months:["Leden","Únor","Březen","Duben","Květen","Červen","Červenec","Srpen","Září","Říjen","Listopad","Prosinec"],monthsShort:["Led","Úno","Bře","Dub","Kvě","Čer","Čnc","Srp","Zář","Říj","Lis","Pro"],today:"Dnes",clear:"Vymazat",monthsTitle:"Měsíc",weekStart:1,format:"dd.mm.yyyy"}}(jQuery);
|
||||
@@ -1 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.da={days:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],daysShort:["søn","man","tir","ons","tor","fre","lør"],daysMin:["sø","ma","ti","on","to","fr","lø"],months:["januar","februar","marts","april","maj","juni","juli","august","september","oktober","november","december"],monthsShort:["jan","feb","mar","apr","maj","jun","jul","aug","sep","okt","nov","dec"],today:"I Dag",clear:"Nulstil"}}(jQuery);
|
||||
!function(a){a.fn.datepicker.dates.da={days:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],daysShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],daysMin:["Sø","Ma","Ti","On","To","Fr","Lø"],months:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthsShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],today:"I Dag",weekStart:1,clear:"Nulstil",format:"dd/mm/yyyy",monthsTitle:"Måneder"}}(jQuery);
|
||||
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.en-CA.min.js
vendored
Normal file
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.en-CA.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates["en-CA"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:0,format:"yyyy-mm-dd"}}(jQuery);
|
||||
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.en-IE.min.js
vendored
Normal file
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.en-IE.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates["en-IE"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);
|
||||
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.en-NZ.min.js
vendored
Normal file
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.en-NZ.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates["en-NZ"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"d/mm/yyyy"}}(jQuery);
|
||||
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.en-ZA.min.js
vendored
Normal file
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.en-ZA.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates["en-ZA"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"yyyy/mm/d"}}(jQuery);
|
||||
@@ -1 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.eu={days:["Igandea","Astelehena","Asteartea","Asteazkena","Osteguna","Ostirala","Larunbata"],daysShort:["Ig","Al","Ar","Az","Og","Ol","Lr"],daysMin:["Ig","Al","Ar","Az","Og","Ol","Lr"],months:["Urtarrila","Otsaila","Martxoa","Apirila","Maiatza","Ekaina","Uztaila","Abuztua","Iraila","Urria","Azaroa","Abendua"],monthsShort:["Urt","Ots","Mar","Api","Mai","Eka","Uzt","Abu","Ira","Urr","Aza","Abe"],today:"Gaur"}}(jQuery);
|
||||
!function(a){a.fn.datepicker.dates.eu={days:["Igandea","Astelehena","Asteartea","Asteazkena","Osteguna","Ostirala","Larunbata"],daysShort:["Ig","Al","Ar","Az","Og","Ol","Lr"],daysMin:["Ig","Al","Ar","Az","Og","Ol","Lr"],months:["Urtarrila","Otsaila","Martxoa","Apirila","Maiatza","Ekaina","Uztaila","Abuztua","Iraila","Urria","Azaroa","Abendua"],monthsShort:["Urt","Ots","Mar","Api","Mai","Eka","Uzt","Abu","Ira","Urr","Aza","Abe"],today:"Gaur",monthsTitle:"Hilabeteak",clear:"Ezabatu",weekStart:1,format:"yyyy/mm/dd"}}(jQuery);
|
||||
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.hi.min.js
vendored
Normal file
1
inst/www/shared/datepicker/js/locales/bootstrap-datepicker.hi.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.hi={days:["रविवार","सोमवार","मंगलवार","बुधवार","गुरुवार","शुक्रवार","शनिवार"],daysShort:["सूर्य","सोम","मंगल","बुध","गुरु","शुक्र","शनि"],daysMin:["र","सो","मं","बु","गु","शु","श"],months:["जनवरी","फ़रवरी","मार्च","अप्रैल","मई","जून","जुलाई","अगस्त","सितम्बर","अक्टूबर","नवंबर","दिसम्बर"],monthsShort:["जन","फ़रवरी","मार्च","अप्रैल","मई","जून","जुलाई","अगस्त","सितं","अक्टूबर","नवं","दिसम्बर"],today:"आज",monthsTitle:"महीने",clear:"साफ",weekStart:1,format:"dd / mm / yyyy"}}(jQuery);
|
||||
@@ -1 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.hy={days:["Կիրակի","Երկուշաբթի","Երեքշաբթի","Չորեքշաբթի","Հինգշաբթի","Ուրբաթ","Շաբաթ"],daysShort:["Կրկ","Երկ","Երք","Չրք","Հնգ","Ուր","Շբթ"],daysMin:["Կրկ","Երկ","Երք","Չրք","Հնգ","Ուր","Շբթ"],months:["Հունվար","Փետրվար","Մարտ","Ապրիլ","Մայիս","Հունիս","Հուլիս","Օգոստոս","Սեպտեմբեր","Հոկտեմբեր","Նոյեմբեր","Դեկտեմբեր"],monthsShort:["Հուն","Փետ","Մար","Ապր","Մայ","Հնս","Հլս","Օգս","Սեպ","Հոկ","Նմբ","Դեկ"],today:"Այսօր",clear:"Ջնջել",format:"dd.mm.yyyy",weekStart:1}}(jQuery);
|
||||
!function(a){a.fn.datepicker.dates.hy={days:["Կիրակի","Երկուշաբթի","Երեքշաբթի","Չորեքշաբթի","Հինգշաբթի","Ուրբաթ","Շաբաթ"],daysShort:["Կիր","Երկ","Երե","Չոր","Հին","Ուրբ","Շաբ"],daysMin:["Կի","Եկ","Եք","Չո","Հի","Ու","Շա"],months:["Հունվար","Փետրվար","Մարտ","Ապրիլ","Մայիս","Հունիս","Հուլիս","Օգոստոս","Սեպտեմբեր","Հոկտեմբեր","Նոյեմբեր","Դեկտեմբեր"],monthsShort:["Հնվ","Փետ","Մար","Ապր","Մայ","Հուն","Հուլ","Օգս","Սեպ","Հոկ","Նոյ","Դեկ"],today:"Այսօր",clear:"Ջնջել",format:"dd.mm.yyyy",weekStart:1,monthsTitle:"Ամիսնէր"}}(jQuery);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user