mirror of
https://github.com/rstudio/shiny.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
17 Commits
rc-v1.12.1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bcf877835 | ||
|
|
719f3c8b3b | ||
|
|
e07298a728 | ||
|
|
75a63716e5 | ||
|
|
b240b0b868 | ||
|
|
3c18aca49b | ||
|
|
9b78be1106 | ||
|
|
3a130b2015 | ||
|
|
27ddc696dc | ||
|
|
4d787c767c | ||
|
|
e161f2e4a8 | ||
|
|
ca259ab0f1 | ||
|
|
9e9a3bf80b | ||
|
|
07af5f91c8 | ||
|
|
fda6a9fede | ||
|
|
d2245a2e34 | ||
|
|
a12a8130b8 |
@@ -34,3 +34,4 @@
|
||||
^.claude$
|
||||
^README-npm\.md$
|
||||
^CRAN-SUBMISSION$
|
||||
^LICENSE\.md$
|
||||
|
||||
4
.github/workflows/R-CMD-check.yaml
vendored
4
.github/workflows/R-CMD-check.yaml
vendored
@@ -19,7 +19,3 @@ jobs:
|
||||
uses: rstudio/shiny-workflows/.github/workflows/routine.yaml@v1
|
||||
R-CMD-check:
|
||||
uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1
|
||||
with:
|
||||
# On R 4.2, Cairo has difficulty installing
|
||||
# Remove this line when https://github.com/s-u/Cairo/issues/52 is merged
|
||||
extra-packages: Cairo=?ignore
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Type: Package
|
||||
Package: shiny
|
||||
Title: Web Application Framework for R
|
||||
Version: 1.12.1
|
||||
Version: 1.13.0.9000
|
||||
Authors@R: c(
|
||||
person("Winston", "Chang", , "winston@posit.co", role = "aut",
|
||||
comment = c(ORCID = "0000-0002-1576-2126")),
|
||||
@@ -69,12 +69,12 @@ Description: Makes it incredibly easy to build interactive web
|
||||
applications with R. Automatic "reactive" binding between inputs and
|
||||
outputs and extensive prebuilt widgets make it possible to build
|
||||
beautiful, responsive, and powerful applications with minimal effort.
|
||||
License: GPL-3 | file LICENSE
|
||||
License: MIT + file LICENSE
|
||||
URL: https://shiny.posit.co/, https://github.com/rstudio/shiny
|
||||
BugReports: https://github.com/rstudio/shiny/issues
|
||||
Depends:
|
||||
methods,
|
||||
R (>= 3.0.2)
|
||||
R (>= 3.1.2)
|
||||
Imports:
|
||||
bslib (>= 0.6.0),
|
||||
cachem (>= 1.1.0),
|
||||
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# MIT License
|
||||
|
||||
Copyright (c) 2025 shiny authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
1011
LICENSE.note
Normal file
1011
LICENSE.note
Normal file
File diff suppressed because it is too large
Load Diff
33
NEWS.md
33
NEWS.md
@@ -1,3 +1,34 @@
|
||||
# shiny (development version)
|
||||
|
||||
## Improvements
|
||||
|
||||
* Output resize/visibility detection now uses native browser observers
|
||||
(`ResizeObserver`, `IntersectionObserver`) instead of relying on jQuery
|
||||
`shown`/`hidden` events and `window.resize`. This makes Shiny's client-side
|
||||
output-info pipeline (image/plot sizing, hidden-state tracking, theme
|
||||
reporting) work automatically in any layout — including CSS-only show/hide,
|
||||
third-party tab components, and non-Bootstrap frameworks — without requiring
|
||||
custom event hooks. This also introduces a `shiny:themechange` event
|
||||
for code that needs to trigger theme clientdata refreshes after changing
|
||||
surrounding visual theme context. (#3682)
|
||||
|
||||
# shiny 1.13.0
|
||||
|
||||
## New features
|
||||
|
||||
* Shiny now supports interactive breakpoints when used with Ark (e.g. in
|
||||
Positron). (#4352)
|
||||
|
||||
## Bug fixes and minor improvements
|
||||
|
||||
* Stack traces from render functions (e.g., `renderPlot()`, `renderDataTable()`)
|
||||
now hide internal Shiny rendering pipeline frames, making error messages
|
||||
cleaner and more focused on user code. (#4358)
|
||||
|
||||
* Fixed an issue with `actionLink()` that extended the link underline to
|
||||
whitespace around the text. (#4348)
|
||||
|
||||
|
||||
# shiny 1.12.1
|
||||
|
||||
## New features
|
||||
@@ -796,7 +827,7 @@ This release features plot caching, an important new tool for improving performa
|
||||
|
||||
### Minor new features and improvements
|
||||
|
||||
* Upgrade FontAwesome from 4.7.0 to 5.3.1 and made `icon` tags browsable, which means they will display in a web browser or RStudio viewer by default (#2186). Note that if your application or library depends on FontAwesome directly using custom CSS, you may need to make some or all of the changes recommended in [Upgrade from Version 4](https://docs.fontawesome.com/v5/web/setup/upgrade-from-v4). Font Awesome icons can also now be used in static R Markdown documents.
|
||||
* Upgrade FontAwesome from 4.7.0 to 5.3.1 and made `icon` tags browsable, which means they will display in a web browser or RStudio viewer by default (#2186). Note that if your application or library depends on FontAwesome directly using custom CSS, you may need to make some or all of the changes recommended in [Upgrade from Version 4](https://docs-v5.fontawesome.com/web/setup/upgrade-from-v4). Font Awesome icons can also now be used in static R Markdown documents.
|
||||
|
||||
* Address #174: Added `datesdisabled` and `daysofweekdisabled` as new parameters to `dateInput()`. This resolves #174 and exposes the underlying arguments of [Bootstrap Datepicker](http://bootstrap-datepicker.readthedocs.io/en/latest/options.html#datesdisabled). `datesdisabled` expects a character vector with values in `yyyy/mm/dd` format and `daysofweekdisabled` expects an integer vector with day interger ids (Sunday=0, Saturday=6). The default value for both is `NULL`, which leaves all days selectable. Thanks, @nathancday! (#2147)
|
||||
|
||||
|
||||
193
R/conditions.R
193
R/conditions.R
@@ -87,13 +87,61 @@ getCallNamesForHash <- function(calls) {
|
||||
})
|
||||
}
|
||||
|
||||
# Get the preferred filename from a srcfile object.
|
||||
#
|
||||
# For user code, prefer the original path (as typed by user, potentially a
|
||||
# symlink or relative path) over the normalized absolute path.
|
||||
#
|
||||
# For package files (under .libPaths()), keep the srcfile$filename because
|
||||
# when a package is installed with keep.source.pkgs = TRUE, the original
|
||||
# srcfilecopy filename may point to a collated build-time path rather than
|
||||
# the real installed package path.
|
||||
getSrcfileFilename <- function(srcfile) {
|
||||
if (!is.null(srcfile$original) &&
|
||||
!is.null(srcfile$original$filename) &&
|
||||
!isPackageFile(srcfile$filename)) {
|
||||
srcfile$original$filename
|
||||
} else {
|
||||
srcfile$filename
|
||||
}
|
||||
}
|
||||
|
||||
# Get the source lines and correct line number from a srcfile + srcref.
|
||||
#
|
||||
# sourceUTF8() wraps user code with a `#line` directive that remaps line
|
||||
# numbers. This means srcref[1] (the remapped line) may not correctly index
|
||||
# into the srcfile's $lines. When a #line directive is present, R extends
|
||||
# the srcref to 8 elements: [7] and [8] are the original (pre-remap) first
|
||||
# and last line numbers in the srcfilecopy's coordinate system.
|
||||
#
|
||||
# Additionally, when the #line path differs from the srcfilecopy filename
|
||||
# (e.g. macOS /tmp -> /private/tmp, or Windows path normalization), R wraps
|
||||
# the srcfile in a srcfilealias whose $lines is NULL. In that case, we
|
||||
# retrieve lines from the original srcfilecopy via $original.
|
||||
getSrcfileLines <- function(srcfile, srcref) {
|
||||
lines <- srcfile$lines
|
||||
line_num <- srcref[1]
|
||||
|
||||
if (is.null(lines) && inherits(srcfile, "srcfilealias")) {
|
||||
lines <- srcfile$original$lines
|
||||
}
|
||||
|
||||
# Use the pre-remap line number when available and different from the
|
||||
# remapped line, indicating a #line directive shifted line numbering.
|
||||
if (isTRUE(length(srcref) >= 7 && srcref[7] != srcref[1])) {
|
||||
line_num <- srcref[7]
|
||||
}
|
||||
|
||||
list(lines = lines, line_num = line_num)
|
||||
}
|
||||
|
||||
getLocs <- function(calls) {
|
||||
vapply(calls, function(call) {
|
||||
srcref <- attr(call, "srcref", exact = TRUE)
|
||||
if (!is.null(srcref)) {
|
||||
srcfile <- attr(srcref, "srcfile", exact = TRUE)
|
||||
if (!is.null(srcfile) && !is.null(srcfile$filename)) {
|
||||
loc <- paste0(srcfile$filename, "#", srcref[[1]])
|
||||
loc <- paste0(getSrcfileFilename(srcfile), "#", srcref[[1]])
|
||||
return(paste0(" [", loc, "]"))
|
||||
}
|
||||
}
|
||||
@@ -101,13 +149,36 @@ getLocs <- function(calls) {
|
||||
}, character(1))
|
||||
}
|
||||
|
||||
# Check if a file path is in an R package library
|
||||
isPackageFile <- function(filepath) {
|
||||
if (is.null(filepath) || filepath == "") {
|
||||
return(FALSE)
|
||||
}
|
||||
|
||||
# Normalize paths for comparison
|
||||
filepath <- normalizePath(filepath, winslash = "/", mustWork = FALSE)
|
||||
lib_paths <- normalizePath(.libPaths(), winslash = "/", mustWork = FALSE)
|
||||
# Ensure trailing slash for proper path-boundary matching, otherwise
|
||||
# e.g. "/usr/lib/R" would incorrectly match "/usr/lib/Rcpp/..."
|
||||
lib_paths <- paste0(sub("/$", "", lib_paths), "/")
|
||||
|
||||
# Check if the file is under any library path
|
||||
any(vapply(
|
||||
lib_paths,
|
||||
function(lib) identical(substr(filepath, 1, nchar(lib)), lib),
|
||||
logical(1)
|
||||
))
|
||||
}
|
||||
|
||||
getCallCategories <- function(calls) {
|
||||
vapply(calls, function(call) {
|
||||
srcref <- attr(call, "srcref", exact = TRUE)
|
||||
if (!is.null(srcref)) {
|
||||
srcfile <- attr(srcref, "srcfile", exact = TRUE)
|
||||
if (!is.null(srcfile)) {
|
||||
if (!is.null(srcfile$original)) {
|
||||
if (!is.null(srcfile) && !is.null(srcfile$filename)) {
|
||||
# Use the absolute path for package detection (srcfile$filename)
|
||||
# rather than the original path which might be relative
|
||||
if (isPackageFile(srcfile$filename)) {
|
||||
return("pkg")
|
||||
} else {
|
||||
return("user")
|
||||
@@ -445,45 +516,93 @@ printOneStackTrace <- function(stackTrace, stripResult, full, offset) {
|
||||
invisible(st)
|
||||
}
|
||||
|
||||
# Filter stack traces using fence markers to hide internal Shiny frames.
|
||||
#
|
||||
# `stackTraces` is a list of character vectors (call names), one per "segment".
|
||||
# A single synchronous error produces one segment (the immediate call stack).
|
||||
# Asynchronous errors (e.g. from promises) produce multiple segments: the deep
|
||||
# stack trace segments come first, then the current segment last. Each deep
|
||||
# segment may begin with frames that overlap the previous segment; a
|
||||
# `..stacktracefloor..` marker delimits this redundant prefix from the active
|
||||
# portion.
|
||||
#
|
||||
# Within the active frames, `..stacktraceon..` / `..stacktraceoff..` markers
|
||||
# act as fences. Frames between a matched off/on pair (reading innermost to
|
||||
# outermost) are hidden — these are the internal rendering pipeline frames
|
||||
# that users don't need to see. The algorithm uses a *reverse clamped cumulative
|
||||
# sum* so that an unmatched `..stacktraceoff..` (one with no corresponding
|
||||
# inner `..stacktraceon..`) is a no-op, preventing it from hiding user frames.
|
||||
# Fence matching works globally across segments so that a `..stacktraceoff..`
|
||||
# at the end of one segment can pair with a `..stacktraceon..` at the start
|
||||
# of the next.
|
||||
stripStackTraces <- function(stackTraces, values = FALSE) {
|
||||
score <- 1L # >=1: show, <=0: hide
|
||||
lapply(seq_along(stackTraces), function(i) {
|
||||
res <- stripOneStackTrace(stackTraces[[i]], i != 1, score)
|
||||
score <<- res$score
|
||||
toShow <- as.logical(res$trace)
|
||||
if (values) {
|
||||
as.character(stackTraces[[i]][toShow])
|
||||
} else {
|
||||
as.logical(toShow)
|
||||
n_segs <- length(stackTraces)
|
||||
if (n_segs == 0L) return(list())
|
||||
|
||||
# Replace NULL segments with empty character vectors
|
||||
stackTraces <- lapply(stackTraces, function(st) st %||% character(0))
|
||||
seg_lengths <- lengths(stackTraces)
|
||||
total <- sum(seg_lengths)
|
||||
|
||||
if (total == 0L) {
|
||||
return(lapply(seg_lengths, function(n) {
|
||||
if (values) character(0) else logical(0)
|
||||
}))
|
||||
}
|
||||
|
||||
# Pre-compute segment boundaries (used in steps 1 and 4)
|
||||
seg_ends <- cumsum(seg_lengths)
|
||||
seg_starts <- c(1L, seg_ends[-n_segs] + 1L)
|
||||
|
||||
# Concatenate all segments into one vector for vectorized operations
|
||||
all <- unlist(stackTraces)
|
||||
|
||||
# 1. Identify prefix elements (at/before last ..stacktracefloor.. in segs 2+)
|
||||
# Prefix elements are always hidden and excluded from fence scoring.
|
||||
is_active <- rep.int(TRUE, total)
|
||||
if (n_segs >= 2L) {
|
||||
for (i in 2:n_segs) {
|
||||
if (seg_lengths[i] == 0L) next
|
||||
seg_idx <- seg_starts[i]:seg_ends[i]
|
||||
floor_pos <- which(all[seg_idx] == "..stacktracefloor..")
|
||||
if (length(floor_pos)) {
|
||||
is_active[seg_idx[seq_len(floor_pos[length(floor_pos)])]] <- FALSE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Compute fence scores and marker mask (vectorized across all segments)
|
||||
is_on <- all == "..stacktraceon.."
|
||||
is_off <- all == "..stacktraceoff.."
|
||||
is_marker <- is_on | is_off | (all == "..stacktracefloor..")
|
||||
scores <- integer(total)
|
||||
scores[is_active & is_on] <- 1L
|
||||
scores[is_active & is_off] <- -1L
|
||||
|
||||
# 3. Reverse clamped cumsum across all segments.
|
||||
# Process from innermost (right) to outermost (left). ..stacktraceon.. (+1)
|
||||
# opens a hidden region working outward, ..stacktraceoff.. (-1) closes it.
|
||||
# Clamping at 0 means an unmatched ..stacktraceoff.. (one with no inner
|
||||
# ..stacktraceon..) is a no-op. Prefix elements have score 0 and pass the
|
||||
# running total through unchanged.
|
||||
#
|
||||
# Vectorized via the identity: clamped_cumsum = cumsum - pmin(0, cummin(cumsum))
|
||||
rs <- rev(scores)
|
||||
cs <- cumsum(rs)
|
||||
depth <- rev(cs - pmin.int(0L, cummin(cs)))
|
||||
|
||||
# 4. Compute visibility (vectorized) and split back into segments
|
||||
toShow <- is_active & depth == 0L & !is_marker
|
||||
|
||||
lapply(seq_len(n_segs), function(i) {
|
||||
if (seg_lengths[i] == 0L) {
|
||||
if (values) return(character(0)) else return(logical(0))
|
||||
}
|
||||
idx <- seg_starts[i]:seg_ends[i]
|
||||
if (values) as.character(all[idx[toShow[idx]]]) else toShow[idx]
|
||||
})
|
||||
}
|
||||
|
||||
stripOneStackTrace <- function(stackTrace, truncateFloor, startingScore) {
|
||||
prefix <- logical(0)
|
||||
if (truncateFloor) {
|
||||
indexOfFloor <- utils::tail(which(stackTrace == "..stacktracefloor.."), 1)
|
||||
if (length(indexOfFloor)) {
|
||||
stackTrace <- stackTrace[(indexOfFloor+1L):length(stackTrace)]
|
||||
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))
|
||||
}
|
||||
|
||||
# Given sys.parents() (which corresponds to sys.calls()), return a logical index
|
||||
# that prunes each subtree so that only the final branch remains. The result,
|
||||
# when applied to sys.calls(), is a linear list of calls without any "wrapper"
|
||||
|
||||
@@ -59,11 +59,11 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL,
|
||||
icon <- validateIcon(icon)
|
||||
|
||||
if (!is.null(icon)) {
|
||||
icon <- span(icon, class = "action-icon")
|
||||
icon <- span(icon, class = "action-icon", .noWS = c("outside", "inside"))
|
||||
}
|
||||
|
||||
if (!is.null(label)) {
|
||||
label <- span(label, class = "action-label")
|
||||
label <- span(label, class = "action-label", .noWS = c("outside", "inside"))
|
||||
}
|
||||
|
||||
tags$button(
|
||||
@@ -74,6 +74,7 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL,
|
||||
`data-val` = value,
|
||||
disabled = if (isTRUE(disabled)) NA else NULL,
|
||||
icon, label,
|
||||
.noWS = "inside",
|
||||
...
|
||||
)
|
||||
}
|
||||
@@ -86,11 +87,11 @@ actionLink <- function(inputId, label, icon = NULL, ...) {
|
||||
icon <- validateIcon(icon)
|
||||
|
||||
if (!is.null(icon)) {
|
||||
icon <- span(icon, class = "action-icon")
|
||||
icon <- span(icon, class = "action-icon", .noWS = c("outside", "inside"))
|
||||
}
|
||||
|
||||
if (!is.null(label)) {
|
||||
label <- span(label, class = "action-label")
|
||||
label <- span(label, class = "action-label", .noWS = c("outside", "inside"))
|
||||
}
|
||||
|
||||
tags$a(
|
||||
@@ -99,6 +100,7 @@ actionLink <- function(inputId, label, icon = NULL, ...) {
|
||||
class = "action-button action-link",
|
||||
`data-val` = value,
|
||||
icon, label,
|
||||
.noWS = "inside",
|
||||
...
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ otel_srcref_attributes <- function(srcref, fn_name = NULL) {
|
||||
# Semantic conventions for code: https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/
|
||||
#
|
||||
# Inspiration from https://github.com/r-lib/testthat/pull/2087/files#diff-92de3306849d93d6f7e76c5aaa1b0c037e2d716f72848f8a1c70536e0c8a1564R123-R124
|
||||
filename <- attr(srcref, "srcfile")$filename
|
||||
srcfile <- attr(srcref, "srcfile")
|
||||
# Prefer the original filename (as user typed it) over the normalized path
|
||||
filename <- getSrcfileFilename(srcfile)
|
||||
dropNulls(list(
|
||||
"code.function.name" = fn_name,
|
||||
# Location attrs
|
||||
|
||||
@@ -304,20 +304,23 @@ rassignSrcrefToLabel <- function(
|
||||
if (is.null(srcfile))
|
||||
return(defaultLabel)
|
||||
|
||||
if (is.null(srcfile$lines))
|
||||
src <- getSrcfileLines(srcfile, srcref)
|
||||
lines <- src$lines
|
||||
line_num <- src$line_num
|
||||
|
||||
if (is.null(lines))
|
||||
return(defaultLabel)
|
||||
|
||||
lines <- srcfile$lines
|
||||
# When pasting at the Console, srcfile$lines is not split
|
||||
if (length(lines) == 1) {
|
||||
lines <- strsplit(lines, "\n")[[1]]
|
||||
}
|
||||
|
||||
if (length(lines) < srcref[1]) {
|
||||
if (length(lines) < line_num) {
|
||||
return(defaultLabel)
|
||||
}
|
||||
|
||||
firstLine <- substring(lines[srcref[1]], srcref[2] - 1)
|
||||
firstLine <- substring(lines[line_num], srcref[2] - 1)
|
||||
|
||||
m <- regexec(
|
||||
# Require the first assignment within the line
|
||||
@@ -1160,20 +1163,23 @@ rexprSrcrefToLabel <- function(srcref, defaultLabel, fnName) {
|
||||
if (is.null(srcfile))
|
||||
return(defaultLabel)
|
||||
|
||||
if (is.null(srcfile$lines))
|
||||
src <- getSrcfileLines(srcfile, srcref)
|
||||
lines <- src$lines
|
||||
line_num <- src$line_num
|
||||
|
||||
if (is.null(lines))
|
||||
return(defaultLabel)
|
||||
|
||||
lines <- srcfile$lines
|
||||
# When pasting at the Console, srcfile$lines is not split
|
||||
if (length(lines) == 1) {
|
||||
lines <- strsplit(lines, "\n")[[1]]
|
||||
}
|
||||
|
||||
if (length(lines) < srcref[1]) {
|
||||
if (length(lines) < line_num) {
|
||||
return(defaultLabel)
|
||||
}
|
||||
|
||||
firstLine <- substring(lines[srcref[1]], 1, srcref[2] - 1)
|
||||
firstLine <- substring(lines[line_num], 1, srcref[2] - 1)
|
||||
|
||||
# Require the assignment to be parsed from the start
|
||||
m <- regexec(paste0("^(.*)(<<-|<-|=)\\s*", fnName, "\\s*\\($"), firstLine)
|
||||
|
||||
10
R/runapp.R
10
R/runapp.R
@@ -216,8 +216,14 @@ runApp <- function(
|
||||
# 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!')
|
||||
rlang::warn(c(
|
||||
sprintf(
|
||||
"Shiny Server v%s or later is required; please upgrade.",
|
||||
.shinyServerMinVersion
|
||||
),
|
||||
"i" = "If you are not using Shiny Server, you are likely seeing this message because the `SHINY_PORT` environment variable is set in your environment.",
|
||||
"i" = "Avoid using `SHINY_PORT` to prevent this warning."
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,8 +130,10 @@ markRenderFunction <- function(
|
||||
# stop warning from happening again for the same object
|
||||
hasExecuted$set(TRUE)
|
||||
}
|
||||
if (is.null(formals(renderFunc))) renderFunc()
|
||||
else renderFunc(...)
|
||||
..stacktraceoff..(
|
||||
if (is.null(formals(renderFunc))) renderFunc()
|
||||
else renderFunc(...)
|
||||
)
|
||||
}
|
||||
|
||||
otelAttrs <-
|
||||
@@ -275,7 +277,7 @@ createRenderFunction <- function(
|
||||
) {
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
hybrid_chain(
|
||||
func(),
|
||||
..stacktraceon..(func()),
|
||||
function(value) {
|
||||
transform(value, shinysession, name, ...)
|
||||
}
|
||||
@@ -628,7 +630,7 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
domain <- createRenderPrintPromiseDomain(width)
|
||||
hybrid_chain(
|
||||
{
|
||||
with_promise_domain(domain, func())
|
||||
with_promise_domain(domain, ..stacktraceon..(func()))
|
||||
},
|
||||
function(value) {
|
||||
res <- withVisible(value)
|
||||
@@ -963,7 +965,7 @@ legacyRenderDataTable <- function(expr, options = NULL, searchDelay = 500,
|
||||
options <- checkDT9(options)
|
||||
res <- checkAsIs(options)
|
||||
hybrid_chain(
|
||||
func(),
|
||||
..stacktraceon..(func()),
|
||||
function(data) {
|
||||
if (length(dim(data)) != 2) return() # expects a rectangular data object
|
||||
if (is.data.frame(data)) data <- as.data.frame(data)
|
||||
|
||||
77
R/utils.R
77
R/utils.R
@@ -1366,31 +1366,62 @@ tryNativeEncoding <- function(string) {
|
||||
if (identical(enc2utf8(string2), string)) string2 else string
|
||||
}
|
||||
|
||||
# similarly, try to source() a file with UTF-8
|
||||
sourceUTF8 <- function(file, envir = globalenv()) {
|
||||
lines <- readUTF8(file)
|
||||
enc <- if (any(Encoding(lines) == 'UTF-8')) 'UTF-8' else 'unknown'
|
||||
src <- srcfilecopy(file, lines, isFile = TRUE) # source reference info
|
||||
# oddly, parse(file) does not work when file contains multibyte chars that
|
||||
# **can** be encoded natively on Windows (might be a bug in base R); we
|
||||
# rewrite the source code in a natively encoded temp file and parse it in this
|
||||
# case (the source reference is still pointed to the original file, though)
|
||||
if (isWindows() && enc == 'unknown') {
|
||||
file <- tempfile(); on.exit(unlink(file), add = TRUE)
|
||||
writeLines(lines, file)
|
||||
}
|
||||
exprs <- try(parse(file, keep.source = FALSE, srcfile = src, encoding = enc))
|
||||
if (inherits(exprs, "try-error")) {
|
||||
diagnoseCode(file)
|
||||
stop("Error sourcing ", file)
|
||||
maybeAnnotateSourceForArk <- function(file, lines) {
|
||||
ark_annotate_source <- get0(".ark_annotate_source", baseenv())
|
||||
|
||||
if (is.null(ark_annotate_source)) {
|
||||
return(lines)
|
||||
}
|
||||
|
||||
# Wrap the exprs in first `{`, then ..stacktraceon..(). It's only really the
|
||||
# ..stacktraceon..() that we care about, but the `{` is needed to make that
|
||||
# possible.
|
||||
exprs <- makeCall(`{`, exprs)
|
||||
# Need to wrap exprs in a list because we want it treated as a single argument
|
||||
exprs <- makeCall(..stacktraceon.., list(exprs))
|
||||
file <- normalizePath(file, mustWork = TRUE, winslash = "/") # Just to be safe
|
||||
uri <- paste0("file:///", sub("^/", "", file)) # Ark expects URIs
|
||||
lines_str <- paste(lines, collapse = "\n")
|
||||
tryCatch(
|
||||
{
|
||||
annotated <- ark_annotate_source(lines_str, uri)
|
||||
if (!is.null(annotated)) {
|
||||
lines <- strsplit(annotated, "\n", fixed = TRUE)[[1]]
|
||||
}
|
||||
},
|
||||
error = function(cnd) {
|
||||
rlang::warn("Can't inject breakpoints for Ark", parent = cnd)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
# similarly, try to source() a file with UTF-8
|
||||
sourceUTF8 <- function(file, envir = globalenv()) {
|
||||
file_norm <- normalizePath(file, mustWork = TRUE, winslash = "/")
|
||||
lines <- readUTF8(file)
|
||||
enc <- if (any(Encoding(lines) == 'UTF-8')) 'UTF-8' else 'unknown'
|
||||
|
||||
# Inject Ark annotations for breakpoints if available
|
||||
lines <- maybeAnnotateSourceForArk(file, lines)
|
||||
|
||||
# Wrap in `..stacktraceon..({...})` using string manipulation before parsing,
|
||||
# with a `#line` directive to map source references back to the original file
|
||||
lines <- c(
|
||||
"..stacktraceon..({",
|
||||
sprintf('#line 1 "%s"', file_norm),
|
||||
lines,
|
||||
"})"
|
||||
)
|
||||
|
||||
# Create a source file copy, i.e. an in-memory srcfile that contains all the
|
||||
# code but refers to an original file
|
||||
src <- srcfilecopy(file, lines, isFile = TRUE)
|
||||
|
||||
# Parse from our annotated lines
|
||||
exprs <- tryCatch(
|
||||
parse(text = lines, keep.source = FALSE, srcfile = src, encoding = enc),
|
||||
error = function(cnd) {
|
||||
diagnoseCode(file)
|
||||
stop("Error sourcing ", file)
|
||||
}
|
||||
)
|
||||
|
||||
eval(exprs, envir)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ We welcome contributions to the **shiny** package. Please see our [CONTRIBUTING.
|
||||
|
||||
## License
|
||||
|
||||
The shiny package as a whole is licensed under the GPLv3. See the [LICENSE](LICENSE) file for more details.
|
||||
The shiny package as a whole is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
|
||||
|
||||
## R version support
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export default [{
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
project: ["./tsconfig.eslint.json"],
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/*! shiny 1.12.1 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
|
||||
/*! shiny 1.13.0.9000 | (c) 2012-2026 Posit Software, PBC. | License: MIT + file LICENSE */
|
||||
:where([data-shiny-busy-spinners] .recalculating){position:relative}[data-shiny-busy-spinners] .recalculating{min-height:var(--shiny-spinner-size, 32px)}[data-shiny-busy-spinners] .recalculating:after{position:absolute;content:"";--_shiny-spinner-url: var(--shiny-spinner-url, url(spinners/ring.svg));--_shiny-spinner-color: var(--shiny-spinner-color, var(--bs-primary, #007bc2));--_shiny-spinner-size: var(--shiny-spinner-size, 32px);--_shiny-spinner-delay: var(--shiny-spinner-delay, 1s);background:var(--_shiny-spinner-color);width:var(--_shiny-spinner-size);height:var(--_shiny-spinner-size);inset:calc(50% - var(--_shiny-spinner-size) / 2);mask-image:var(--_shiny-spinner-url);-webkit-mask-image:var(--_shiny-spinner-url);opacity:0;animation-delay:var(--_shiny-spinner-delay);animation-name:fade-in;animation-duration:.25s;animation-fill-mode:forwards}[data-shiny-busy-spinners] .recalculating:has(>*),[data-shiny-busy-spinners] .recalculating:empty{opacity:1}[data-shiny-busy-spinners] .recalculating>*:not(.recalculating){opacity:var(--_shiny-fade-opacity);transition:opacity .25s ease var(--shiny-spinner-delay, 1s)}[data-shiny-busy-spinners] .recalculating.html-widget-output{visibility:inherit!important}[data-shiny-busy-spinners] .recalculating.html-widget-output>*{visibility:hidden}[data-shiny-busy-spinners] .recalculating.html-widget-output :after{visibility:visible}[data-shiny-busy-spinners] .recalculating.shiny-html-output:not(.shiny-table-output):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) );--_shiny-pulse-height: var(--shiny-pulse-height, 3px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-direction:alternate;animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating:not(.shiny-html-output)):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating.shiny-table-output):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(#shiny-disconnected-overlay):after{display:none}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) );--_shiny-pulse-height: var(--shiny-pulse-height, 3px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-direction:alternate;animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:has(#shiny-disconnected-overlay):after{display:none}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes busy-page-pulse{0%{left:-14%;right:97%}45%{left:0%;right:14%}55%{left:14%;right:0%}to{left:97%;right:-14%}}.shiny-spinner-output-container{--shiny-spinner-size: 0px}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
/*! shiny 1.12.1 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
|
||||
/*! shiny 1.13.0.9000 | (c) 2012-2026 Posit Software, PBC. | License: MIT + file LICENSE */
|
||||
"use strict";(()=>{document.documentElement.classList.add("autoreload-enabled");var c=window.location.protocol==="https:"?"wss:":"ws:",s=window.location.pathname.replace(/\/?$/,"/")+"autoreload/",i=`${c}//${window.location.host}${s}`,l=document.currentScript?.dataset?.wsUrl||i;async function u(o){let e=new WebSocket(o),n=!1;return new Promise((a,r)=>{e.onopen=()=>{n=!0},e.onerror=t=>{r(t)},e.onclose=()=>{n?a(!1):r(new Error("WebSocket connection failed"))},e.onmessage=function(t){t.data==="autoreload"&&a(!0)}})}async function d(o){return new Promise(e=>setTimeout(e,o))}async function w(){for(;;){try{if(await u(l)){window.location.reload();return}}catch{console.debug("Giving up on autoreload");return}await d(1e3)}}w().catch(o=>{console.error(o)});})();
|
||||
//# sourceMappingURL=shiny-autoreload.js.map
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/*! shiny 1.12.1 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
|
||||
/*! shiny 1.13.0.9000 | (c) 2012-2026 Posit Software, PBC. | License: MIT + file LICENSE */
|
||||
#showcase-well{border-radius:0}.shiny-code{background-color:#fff;margin-bottom:0}.shiny-code code{font-family:Menlo,Consolas,Courier New,monospace}.shiny-code-container{margin-top:20px;clear:both}.shiny-code-container h3{display:inline;margin-right:15px}.showcase-header{font-size:16px;font-weight:400}.showcase-code-link{text-align:right;padding:15px}#showcase-app-container{vertical-align:top}#showcase-code-tabs{margin-right:15px}#showcase-code-tabs pre{border:none;line-height:1em}#showcase-code-tabs .nav,#showcase-code-tabs ul{margin-bottom:0}#showcase-code-tabs .tab-content{border-style:solid;border-color:#e5e5e5;border-width:0px 1px 1px 1px;overflow:auto;border-bottom-right-radius:4px;border-bottom-left-radius:4px}#showcase-app-code{width:100%}#showcase-code-position-toggle{float:right}#showcase-sxs-code{padding-top:20px;vertical-align:top}.showcase-code-license{display:block;text-align:right}#showcase-code-content pre{background-color:#fff}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
/*! shiny 1.12.1 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
|
||||
/*! shiny 1.13.0.9000 | (c) 2012-2026 Posit Software, PBC. | License: MIT + file LICENSE */
|
||||
"use strict";(()=>{var m=400;function c(e,s){let t=0;if(e.nodeType===3){let n=e.nodeValue?.replace(/\n/g,"").length??0;if(n>=s)return{element:e,offset:s};t+=n}else if(e.nodeType===1&&e.firstChild){let n=c(e.firstChild,s);if(n.element!==null)return n;t+=n.offset}return e.nextSibling?c(e.nextSibling,s-t):{element:null,offset:t}}function a(e,s,t){let n=0;for(let l=0;l<e.childNodes.length;l++){let i=e.childNodes[l];if(i.nodeType===3){let o=/\n/g,d;for(;(d=o.exec(i.nodeValue))!==null;)if(n++,n===s)return c(i,d.index+t+1)}else if(i.nodeType===1){let o=a(i,s-n,t);if(o.element!==null)return o;n+=o.offset}}return{element:null,offset:n}}function g(e,s){if(!document.createRange)return;let t=document.getElementById("srcref_"+e);if(!t){t=document.createElement("span"),t.id="srcref_"+e;let n=e,l=document.getElementById(s.replace(/\./g,"_")+"_code");if(!l)return;let i=a(l,n[0],n[4]),o=a(l,n[2],n[5]);if(i.element===null||o.element===null)return;let d=document.createRange();i.element.parentNode?.nodeName==="SPAN"&&i.element!==o.element?d.setStartBefore(i.element.parentNode):d.setStart(i.element,i.offset),o.element.parentNode?.nodeName==="SPAN"&&i.element!==o.element?d.setEndAfter(o.element.parentNode):d.setEnd(o.element,o.offset),d.surroundContents(t)}$(t).stop(!0,!0).effect("highlight",null,1600)}window.Shiny&&window.Shiny.addCustomMessageHandler("showcase-src",function(e){e.srcref&&e.srcfile&&g(e.srcref,e.srcfile)});var r=!1,u=function(e,s){let t=s?m:1,n=e?document.getElementById("showcase-sxs-code"):document.getElementById("showcase-code-inline"),l=e?document.getElementById("showcase-code-inline"):document.getElementById("showcase-sxs-code");if(document.getElementById("showcase-app-metadata")===null){let o=$("#showcase-well");e?o.fadeOut(t):o.fadeIn(t)}if(n===null||l===null){console.warn("Could not find the host elements for the code tabs. This is likely a bug in the showcase app.");return}$(n).hide(),$(l).fadeOut(t,function(){let o=document.getElementById("showcase-code-tabs");if(o===null){console.warn("Could not find the code tabs element. This is likely a bug in the showcase app.");return}if(l.removeChild(o),n.appendChild(o),e?p():document.getElementById("showcase-code-content")?.removeAttribute("style"),$(n).fadeIn(t),!e&&(document.getElementById("showcase-app-container")?.removeAttribute("style"),s)){let f=$(n).offset()?.top;f!==void 0&&$(document.body).animate({scrollTop:f})}let d=document.getElementById("readme-md");d!==null&&(d.parentElement?.removeChild(d),e?(l.appendChild(d),$(l).fadeIn(t)):document.getElementById("showcase-app-metadata")?.appendChild(d)),document.getElementById("showcase-code-position-toggle").innerHTML=e?'<i class="fa fa-level-down"></i> show below':'<i class="fa fa-level-up"></i> show with app'}),e&&$(document.body).animate({scrollTop:0},t),r=e,h(e&&s),$(window).trigger("resize")};function h(e){let t=960,n=1,l=document.getElementById("showcase-app-code").offsetWidth;l/2>960?t=l/2:l*.66>960?t=960:(t=l*.66,n=t/960),$("#showcase-app-container").animate({width:t+"px",zoom:n*100+"%"},e?m:0)}var w=function(){u(!r,!0)},y=function(){document.body.offsetWidth>1350&&u(!0,!1)};function p(){document.getElementById("showcase-code-content").style.height=$(window).height()+"px"}function E(){let e=document.getElementById("showcase-markdown-content");if(e!==null){let s=document.getElementById("readme-md");if(s!==null){let t=e.content.cloneNode(!0);s.appendChild(t)}}}$(window).resize(function(){r&&(h(!1),p())});window.toggleCodePosition=w;$(window).on("load",y);$(window).on("load",E);window.hljs&&window.hljs.initHighlightingOnLoad();})();
|
||||
//# sourceMappingURL=shiny-showcase.js.map
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
/*! shiny 1.12.1 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
|
||||
/*! shiny 1.13.0.9000 | (c) 2012-2026 Posit Software, PBC. | License: MIT + file LICENSE */
|
||||
"use strict";(()=>{var t=eval;window.addEventListener("message",function(a){let e=a.data;e.code&&t(e.code)});})();
|
||||
//# sourceMappingURL=shiny-testmode.js.map
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*! shiny 1.12.1 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
|
||||
/*! shiny 1.13.0.9000 | (c) 2012-2026 Posit Software, PBC. | License: MIT + file LICENSE */
|
||||
"use strict";
|
||||
(() => {
|
||||
var __create = Object.create;
|
||||
@@ -230,6 +230,10 @@
|
||||
this.args = args;
|
||||
this.$invoke();
|
||||
}
|
||||
cancel() {
|
||||
this.$clearTimer();
|
||||
this.args = null;
|
||||
}
|
||||
isPending() {
|
||||
return this.timerId !== null;
|
||||
}
|
||||
@@ -250,7 +254,7 @@
|
||||
};
|
||||
function debounce(threshold, func) {
|
||||
let timerId = null;
|
||||
return function thisFunc(...args) {
|
||||
const debounced = function thisFunc(...args) {
|
||||
if (timerId !== null) {
|
||||
clearTimeout(timerId);
|
||||
timerId = null;
|
||||
@@ -261,6 +265,13 @@
|
||||
func.apply(thisFunc, args);
|
||||
}, threshold);
|
||||
};
|
||||
debounced.cancel = function() {
|
||||
if (timerId !== null) {
|
||||
clearTimeout(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
};
|
||||
return debounced;
|
||||
}
|
||||
|
||||
// srcts/src/time/invoke.ts
|
||||
@@ -331,22 +342,58 @@
|
||||
}
|
||||
};
|
||||
|
||||
// srcts/src/shiny/sendImageSize.ts
|
||||
var SendImageSize = class {
|
||||
setImageSend(inputBatchSender, doSendImageSize) {
|
||||
const sendImageSizeDebouncer = new Debouncer(null, doSendImageSize, 0);
|
||||
// srcts/src/shiny/sendOutputInfo.ts
|
||||
var _pendingObserverCallbacks;
|
||||
var SendOutputInfo = class {
|
||||
constructor() {
|
||||
__privateAdd(this, _pendingObserverCallbacks, /* @__PURE__ */ new Set());
|
||||
}
|
||||
setSendMethod(inputBatchSender, doSendOutputInfo) {
|
||||
const sendOutputInfoDebouncer = new Debouncer(null, doSendOutputInfo, 0);
|
||||
this.regular = function() {
|
||||
sendImageSizeDebouncer.normalCall();
|
||||
sendOutputInfoDebouncer.normalCall();
|
||||
};
|
||||
inputBatchSender.lastChanceCallback.push(function() {
|
||||
if (sendImageSizeDebouncer.isPending())
|
||||
sendImageSizeDebouncer.immediateCall();
|
||||
inputBatchSender.lastChanceCallback.push(() => {
|
||||
__privateGet(this, _pendingObserverCallbacks).forEach((callback) => callback.flush());
|
||||
if (sendOutputInfoDebouncer.isPending())
|
||||
sendOutputInfoDebouncer.immediateCall();
|
||||
});
|
||||
this.transitioned = debounce(200, this.regular);
|
||||
return sendImageSizeDebouncer;
|
||||
return sendOutputInfoDebouncer;
|
||||
}
|
||||
createObserverCallback(delayMs, callback) {
|
||||
const debouncer = new Debouncer(
|
||||
null,
|
||||
() => {
|
||||
__privateGet(this, _pendingObserverCallbacks).delete(observerCallback);
|
||||
callback();
|
||||
},
|
||||
delayMs
|
||||
);
|
||||
const observerCallback = Object.assign(
|
||||
() => {
|
||||
__privateGet(this, _pendingObserverCallbacks).add(observerCallback);
|
||||
debouncer.normalCall();
|
||||
},
|
||||
{
|
||||
cancel: () => {
|
||||
__privateGet(this, _pendingObserverCallbacks).delete(observerCallback);
|
||||
debouncer.cancel();
|
||||
},
|
||||
flush: () => {
|
||||
__privateGet(this, _pendingObserverCallbacks).delete(observerCallback);
|
||||
if (debouncer.isPending()) {
|
||||
debouncer.immediateCall();
|
||||
}
|
||||
},
|
||||
isPending: () => debouncer.isPending()
|
||||
}
|
||||
);
|
||||
return observerCallback;
|
||||
}
|
||||
};
|
||||
var sendImageSizeFns = new SendImageSize();
|
||||
_pendingObserverCallbacks = new WeakMap();
|
||||
var sendOutputInfoFns = new SendOutputInfo();
|
||||
|
||||
// srcts/src/shiny/singletons.ts
|
||||
var import_jquery4 = __toESM(require_jquery());
|
||||
@@ -543,7 +590,7 @@
|
||||
$head.append(newStyle);
|
||||
oldStyle.remove();
|
||||
removeSheet(oldSheet);
|
||||
sendImageSizeFns.transitioned();
|
||||
sendOutputInfoFns.transitioned();
|
||||
};
|
||||
xhr.send();
|
||||
};
|
||||
@@ -578,7 +625,7 @@
|
||||
$dummyEl.one("transitionend", () => {
|
||||
$dummyEl.remove();
|
||||
removeSheet(oldSheet);
|
||||
sendImageSizeFns.transitioned();
|
||||
sendOutputInfoFns.transitioned();
|
||||
});
|
||||
(0, import_jquery5.default)(document.body).append($dummyEl);
|
||||
const color = "#" + Math.floor(Math.random() * 16777215).toString(16);
|
||||
@@ -811,6 +858,15 @@
|
||||
}
|
||||
return x2;
|
||||
}
|
||||
function isVisible(el) {
|
||||
if (el.offsetWidth !== 0 || el.offsetHeight !== 0) {
|
||||
return true;
|
||||
}
|
||||
if (getStyle(el, "display") === "none") {
|
||||
return false;
|
||||
}
|
||||
return el.parentElement ? isVisible(el.parentElement) : true;
|
||||
}
|
||||
function padZeros(n4, digits) {
|
||||
let str = n4.toString();
|
||||
while (str.length < digits) str = "0" + str;
|
||||
@@ -5749,12 +5805,7 @@ ${duplicateIdMsg}`;
|
||||
}
|
||||
return inputItems;
|
||||
}
|
||||
async function bindOutputs({
|
||||
sendOutputHiddenState,
|
||||
maybeAddThemeObserver,
|
||||
outputBindings,
|
||||
outputIsRecalculating
|
||||
}, scope = document.documentElement) {
|
||||
async function bindOutputs({ outputBindings, outputIsRecalculating }, scope = document.documentElement) {
|
||||
const $scope = (0, import_jquery35.default)(scope);
|
||||
const bindings = outputBindings.getBindings();
|
||||
for (let i5 = 0; i5 < bindings.length; i5++) {
|
||||
@@ -5769,7 +5820,6 @@ ${duplicateIdMsg}`;
|
||||
if ($el.hasClass("shiny-bound-output")) {
|
||||
continue;
|
||||
}
|
||||
maybeAddThemeObserver(el);
|
||||
const bindingAdapter = new OutputBindingAdapter(el, binding);
|
||||
await shinyAppBindOutput(id, bindingAdapter);
|
||||
$el.data("shiny-output-binding", bindingAdapter);
|
||||
@@ -5787,8 +5837,7 @@ ${duplicateIdMsg}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
setTimeout(sendImageSizeFns.regular, 0);
|
||||
setTimeout(sendOutputHiddenState, 0);
|
||||
setTimeout(() => sendOutputInfoFns.regular(), 0);
|
||||
}
|
||||
function unbindInputs(scope = document.documentElement, includeSelf = false) {
|
||||
const inputs = (0, import_jquery35.default)(scope).find(".shiny-bound-input").toArray();
|
||||
@@ -5811,7 +5860,7 @@ ${duplicateIdMsg}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
function unbindOutputs({ sendOutputHiddenState }, scope = document.documentElement, includeSelf = false) {
|
||||
function unbindOutputs(scope = document.documentElement, includeSelf = false) {
|
||||
const outputs = (0, import_jquery35.default)(scope).find(".shiny-bound-output").toArray();
|
||||
if (includeSelf && (0, import_jquery35.default)(scope).hasClass("shiny-bound-output")) {
|
||||
outputs.push(scope);
|
||||
@@ -5825,6 +5874,20 @@ ${duplicateIdMsg}`;
|
||||
bindingsRegistry.removeBinding(id, "output");
|
||||
$el.removeClass("shiny-bound-output");
|
||||
$el.removeData("shiny-output-binding");
|
||||
for (const prefix of [
|
||||
"shiny-resize-observer",
|
||||
"shiny-intersection-observer",
|
||||
"shiny-mutate-observer"
|
||||
]) {
|
||||
const observer = $el.data(prefix);
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
$el.removeData(prefix);
|
||||
}
|
||||
const callback = $el.data(prefix + "-callback");
|
||||
callback?.cancel?.();
|
||||
$el.removeData(prefix + "-callback");
|
||||
}
|
||||
$el.trigger({
|
||||
type: "shiny:unbound",
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
@@ -5832,8 +5895,7 @@ ${duplicateIdMsg}`;
|
||||
bindingType: "output"
|
||||
});
|
||||
}
|
||||
setTimeout(sendImageSizeFns.regular, 0);
|
||||
setTimeout(sendOutputHiddenState, 0);
|
||||
setTimeout(() => sendOutputInfoFns.regular(), 0);
|
||||
}
|
||||
async function _bindAll(shinyCtx, scope) {
|
||||
await bindOutputs(shinyCtx, scope);
|
||||
@@ -5841,9 +5903,9 @@ ${duplicateIdMsg}`;
|
||||
bindingsRegistry.checkValidity(scope);
|
||||
return currentInputs;
|
||||
}
|
||||
function unbindAll(shinyCtx, scope, includeSelf = false) {
|
||||
function unbindAll(scope, includeSelf = false) {
|
||||
unbindInputs(scope, includeSelf);
|
||||
unbindOutputs(shinyCtx, scope, includeSelf);
|
||||
unbindOutputs(scope, includeSelf);
|
||||
}
|
||||
async function bindAll(shinyCtx, scope) {
|
||||
const currentInputItems = await _bindAll(shinyCtx, scope);
|
||||
@@ -7206,7 +7268,7 @@ ${duplicateIdMsg}`;
|
||||
// srcts/src/shiny/index.ts
|
||||
var ShinyClass = class {
|
||||
constructor() {
|
||||
this.version = "1.12.1";
|
||||
this.version = "1.13.0.9000";
|
||||
const { inputBindings, fileInputBinding: fileInputBinding2 } = initInputBindings();
|
||||
const { outputBindings } = initOutputBindings();
|
||||
setFileInputBinding(fileInputBinding2);
|
||||
@@ -7288,8 +7350,6 @@ ${duplicateIdMsg}`;
|
||||
return {
|
||||
inputs,
|
||||
inputsRate,
|
||||
sendOutputHiddenState,
|
||||
maybeAddThemeObserver,
|
||||
inputBindings,
|
||||
outputBindings,
|
||||
initDeferredIframes,
|
||||
@@ -7300,7 +7360,7 @@ ${duplicateIdMsg}`;
|
||||
await bindAll(shinyBindCtx(), scope);
|
||||
};
|
||||
this.unbindAll = function(scope, includeSelf = false) {
|
||||
unbindAll(shinyBindCtx(), scope, includeSelf);
|
||||
unbindAll(scope, includeSelf);
|
||||
};
|
||||
function initializeInputs(scope = document.documentElement) {
|
||||
const bindings = inputBindings.getBindings();
|
||||
@@ -7322,230 +7382,190 @@ ${duplicateIdMsg}`;
|
||||
function getIdFromEl(el) {
|
||||
const $el = (0, import_jquery40.default)(el);
|
||||
const bindingAdapter = $el.data("shiny-output-binding");
|
||||
if (!bindingAdapter) return null;
|
||||
else return bindingAdapter.getId();
|
||||
return bindingAdapter ? bindingAdapter.getId() : null;
|
||||
}
|
||||
initializeInputs(document.documentElement);
|
||||
const initialValues = mapValues(
|
||||
await _bindAll(shinyBindCtx(), document.documentElement),
|
||||
(x2) => x2.value
|
||||
);
|
||||
(0, import_jquery40.default)(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
|
||||
function() {
|
||||
const id = getIdFromEl(this), rect = getBoundingClientSizeBeforeZoom(this);
|
||||
if (rect.width !== 0 || rect.height !== 0) {
|
||||
initialValues[".clientdata_output_" + id + "_width"] = rect.width;
|
||||
initialValues[".clientdata_output_" + id + "_height"] = rect.height;
|
||||
}
|
||||
function setInput(name, value, initial = false) {
|
||||
if (initial) {
|
||||
initialValues[name] = value;
|
||||
} else {
|
||||
inputs.setInput(name, value);
|
||||
}
|
||||
);
|
||||
function getComputedBgColor(el) {
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
const bgColor = getStyle(el, "background-color");
|
||||
if (!bgColor) return bgColor;
|
||||
const m2 = bgColor.match(
|
||||
/^rgba\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/
|
||||
);
|
||||
if (bgColor === "transparent" || m2 && parseFloat(m2[4]) === 0) {
|
||||
const bgImage = getStyle(el, "background-image");
|
||||
if (bgImage && bgImage !== "none") {
|
||||
return null;
|
||||
} else {
|
||||
return getComputedBgColor(el.parentElement);
|
||||
}
|
||||
}
|
||||
return bgColor;
|
||||
}
|
||||
function getComputedFont(el) {
|
||||
const fontFamily = getStyle(el, "font-family");
|
||||
const fontSize = getStyle(el, "font-size");
|
||||
return {
|
||||
families: fontFamily?.replace(/"/g, "").split(", "),
|
||||
size: fontSize
|
||||
};
|
||||
function doSendSize(el, initial = false) {
|
||||
const id = getIdFromEl(el);
|
||||
if (!id) return;
|
||||
const rect = getBoundingClientSizeBeforeZoom(el);
|
||||
if (rect.width !== 0 || rect.height !== 0) {
|
||||
setInput(".clientdata_output_" + id + "_width", rect.width, initial);
|
||||
setInput(".clientdata_output_" + id + "_height", rect.height, initial);
|
||||
}
|
||||
}
|
||||
(0, import_jquery40.default)(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
|
||||
function() {
|
||||
const el = this;
|
||||
const id = getIdFromEl(el);
|
||||
initialValues[".clientdata_output_" + id + "_bg"] = getComputedBgColor(el);
|
||||
initialValues[".clientdata_output_" + id + "_fg"] = getStyle(
|
||||
el,
|
||||
"color"
|
||||
);
|
||||
initialValues[".clientdata_output_" + id + "_accent"] = getComputedLinkColor(el);
|
||||
initialValues[".clientdata_output_" + id + "_font"] = getComputedFont(el);
|
||||
maybeAddThemeObserver(el);
|
||||
}
|
||||
);
|
||||
function maybeAddThemeObserver(el) {
|
||||
if (!window.MutationObserver) {
|
||||
return;
|
||||
}
|
||||
const cl = el.classList;
|
||||
const reportTheme = cl.contains("shiny-image-output") || cl.contains("shiny-plot-output") || cl.contains("shiny-report-theme");
|
||||
if (!reportTheme) {
|
||||
return;
|
||||
}
|
||||
const $el = (0, import_jquery40.default)(el);
|
||||
if ($el.data("shiny-theme-observer")) {
|
||||
return;
|
||||
}
|
||||
const observerCallback = new Debouncer(null, () => doSendTheme(el), 100);
|
||||
const observer = new MutationObserver(
|
||||
() => observerCallback.normalCall()
|
||||
);
|
||||
const config = { attributes: true, attributeFilter: ["style", "class"] };
|
||||
observer.observe(el, config);
|
||||
$el.data("shiny-theme-observer", observer);
|
||||
function doTriggerResize(el) {
|
||||
const $el = (0, import_jquery40.default)(el), binding = $el.data("shiny-output-binding");
|
||||
if (!binding) return;
|
||||
$el.trigger({
|
||||
type: "shiny:visualchange",
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
visible: isVisible(el),
|
||||
binding
|
||||
});
|
||||
binding.onResize();
|
||||
}
|
||||
function doSendTheme(el) {
|
||||
function doSendTheme(el, initial = false) {
|
||||
if (el.classList.contains("shiny-output-error")) {
|
||||
return;
|
||||
}
|
||||
const id = getIdFromEl(el);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_bg",
|
||||
getComputedBgColor(el)
|
||||
);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_fg",
|
||||
getStyle(el, "color")
|
||||
);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_accent",
|
||||
getComputedLinkColor(el)
|
||||
);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_font",
|
||||
getComputedFont(el)
|
||||
);
|
||||
}
|
||||
function doSendImageSize() {
|
||||
(0, import_jquery40.default)(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
|
||||
function() {
|
||||
const id = getIdFromEl(this), rect = getBoundingClientSizeBeforeZoom(this);
|
||||
if (rect.width !== 0 || rect.height !== 0) {
|
||||
inputs.setInput(".clientdata_output_" + id + "_width", rect.width);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_height",
|
||||
rect.height
|
||||
);
|
||||
function getComputedBgColor(el2) {
|
||||
if (!el2) {
|
||||
return null;
|
||||
}
|
||||
const bgColor = getStyle(el2, "background-color");
|
||||
if (!bgColor) return bgColor;
|
||||
const m2 = bgColor.match(
|
||||
/^rgba\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/
|
||||
);
|
||||
if (bgColor === "transparent" || m2 && parseFloat(m2[4]) === 0) {
|
||||
const bgImage = getStyle(el2, "background-image");
|
||||
if (bgImage && bgImage !== "none") {
|
||||
return null;
|
||||
} else {
|
||||
return getComputedBgColor(el2.parentElement);
|
||||
}
|
||||
}
|
||||
);
|
||||
(0, import_jquery40.default)(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
|
||||
function() {
|
||||
doSendTheme(this);
|
||||
}
|
||||
);
|
||||
(0, import_jquery40.default)(".shiny-bound-output").each(function() {
|
||||
const $this = (0, import_jquery40.default)(this), binding = $this.data("shiny-output-binding");
|
||||
$this.trigger({
|
||||
type: "shiny:visualchange",
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
visible: !isHidden(this),
|
||||
binding
|
||||
});
|
||||
binding.onResize();
|
||||
});
|
||||
}
|
||||
sendImageSizeFns.setImageSend(inputBatchSender, doSendImageSize);
|
||||
function isHidden(obj) {
|
||||
if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) {
|
||||
return false;
|
||||
} else if (getStyle(obj, "display") === "none") {
|
||||
return true;
|
||||
} else {
|
||||
return isHidden(obj.parentNode);
|
||||
return bgColor;
|
||||
}
|
||||
}
|
||||
let lastKnownVisibleOutputs = {};
|
||||
(0, import_jquery40.default)(".shiny-bound-output").each(function() {
|
||||
const id = getIdFromEl(this);
|
||||
if (isHidden(this)) {
|
||||
initialValues[".clientdata_output_" + id + "_hidden"] = true;
|
||||
} else {
|
||||
lastKnownVisibleOutputs[id] = true;
|
||||
initialValues[".clientdata_output_" + id + "_hidden"] = false;
|
||||
}
|
||||
});
|
||||
function doSendOutputHiddenState() {
|
||||
const visibleOutputs = {};
|
||||
(0, import_jquery40.default)(".shiny-bound-output").each(function() {
|
||||
const id = getIdFromEl(this);
|
||||
delete lastKnownVisibleOutputs[id];
|
||||
const hidden = isHidden(this), evt = {
|
||||
type: "shiny:visualchange",
|
||||
visible: !hidden
|
||||
function getComputedFont(el2) {
|
||||
const fontFamily = getStyle(el2, "font-family");
|
||||
const fontSize = getStyle(el2, "font-size");
|
||||
return {
|
||||
families: fontFamily?.replace(/"/g, "").split(", "),
|
||||
size: fontSize
|
||||
};
|
||||
if (hidden) {
|
||||
inputs.setInput(".clientdata_output_" + id + "_hidden", true);
|
||||
} else {
|
||||
visibleOutputs[id] = true;
|
||||
inputs.setInput(".clientdata_output_" + id + "_hidden", false);
|
||||
}
|
||||
const $this = (0, import_jquery40.default)(this);
|
||||
evt.binding = $this.data("shiny-output-binding");
|
||||
$this.trigger(evt);
|
||||
});
|
||||
for (const name in lastKnownVisibleOutputs) {
|
||||
if (hasDefinedProperty(lastKnownVisibleOutputs, name))
|
||||
inputs.setInput(".clientdata_output_" + name + "_hidden", true);
|
||||
}
|
||||
lastKnownVisibleOutputs = visibleOutputs;
|
||||
const id = getIdFromEl(el);
|
||||
if (!id) return;
|
||||
setInput(
|
||||
".clientdata_output_" + id + "_bg",
|
||||
getComputedBgColor(el),
|
||||
initial
|
||||
);
|
||||
setInput(
|
||||
".clientdata_output_" + id + "_fg",
|
||||
getStyle(el, "color"),
|
||||
initial
|
||||
);
|
||||
setInput(
|
||||
".clientdata_output_" + id + "_accent",
|
||||
getComputedLinkColor(el),
|
||||
initial
|
||||
);
|
||||
setInput(
|
||||
".clientdata_output_" + id + "_font",
|
||||
getComputedFont(el),
|
||||
initial
|
||||
);
|
||||
}
|
||||
const sendOutputHiddenStateDebouncer = new Debouncer(
|
||||
null,
|
||||
doSendOutputHiddenState,
|
||||
0
|
||||
);
|
||||
function sendOutputHiddenState() {
|
||||
sendOutputHiddenStateDebouncer.normalCall();
|
||||
const visibleOutputs = /* @__PURE__ */ new Set();
|
||||
function doSendHiddenState(el, initial = false) {
|
||||
const id = getIdFromEl(el);
|
||||
if (!id) return;
|
||||
const hidden = !isVisible(el);
|
||||
if (hidden) {
|
||||
visibleOutputs.delete(id);
|
||||
} else {
|
||||
visibleOutputs.add(id);
|
||||
}
|
||||
setInput(".clientdata_output_" + id + "_hidden", hidden, initial);
|
||||
}
|
||||
inputBatchSender.lastChanceCallback.push(function() {
|
||||
if (sendOutputHiddenStateDebouncer.isPending())
|
||||
sendOutputHiddenStateDebouncer.immediateCall();
|
||||
});
|
||||
function filterEventsByNamespace(namespace, handler, ...args) {
|
||||
const namespaceArr = namespace.split(".");
|
||||
return function(e4) {
|
||||
const eventNamespace = e4.namespace?.split(".") ?? [];
|
||||
for (let i5 = 0; i5 < namespaceArr.length; i5++) {
|
||||
if (eventNamespace.indexOf(namespaceArr[i5]) === -1) return;
|
||||
function reportsSize(el) {
|
||||
return el.classList.contains("shiny-image-output") || el.classList.contains("shiny-plot-output") || el.classList.contains("shiny-report-size");
|
||||
}
|
||||
function reportsTheme(el) {
|
||||
return el.classList.contains("shiny-image-output") || el.classList.contains("shiny-plot-output") || el.classList.contains("shiny-report-theme");
|
||||
}
|
||||
function refreshOutputInfo(el, initial = false) {
|
||||
if (!initial) doTriggerResize(el);
|
||||
doSendHiddenState(el, initial);
|
||||
if (reportsSize(el)) doSendSize(el, initial);
|
||||
if (reportsTheme(el)) doSendTheme(el, initial);
|
||||
}
|
||||
function refreshThemeOutputs(initial = false) {
|
||||
(0, import_jquery40.default)(".shiny-bound-output").each(function() {
|
||||
const el = this;
|
||||
if (reportsTheme(el)) doSendTheme(el, initial);
|
||||
});
|
||||
}
|
||||
function registerThemeRefreshSignals() {
|
||||
const scheduleThemeInfoRefresh = sendOutputInfoFns.createObserverCallback(
|
||||
100,
|
||||
() => refreshThemeOutputs()
|
||||
);
|
||||
(0, import_jquery40.default)(window).resize(function() {
|
||||
scheduleThemeInfoRefresh();
|
||||
});
|
||||
(0, import_jquery40.default)(document).on("shiny:themechange", function() {
|
||||
scheduleThemeInfoRefresh();
|
||||
});
|
||||
}
|
||||
function ensureObservers(el) {
|
||||
const $el = (0, import_jquery40.default)(el);
|
||||
if (!$el.data("shiny-resize-observer")) {
|
||||
const onResize = sendOutputInfoFns.createObserverCallback(
|
||||
100,
|
||||
() => refreshOutputInfo(el)
|
||||
);
|
||||
const ro = new ResizeObserver(() => onResize());
|
||||
ro.observe(el);
|
||||
$el.data("shiny-resize-observer-callback", onResize);
|
||||
$el.data("shiny-resize-observer", ro);
|
||||
}
|
||||
if (!$el.data("shiny-intersection-observer")) {
|
||||
const onIntersect = sendOutputInfoFns.createObserverCallback(
|
||||
100,
|
||||
() => refreshOutputInfo(el)
|
||||
);
|
||||
const io = new IntersectionObserver(() => onIntersect());
|
||||
io.observe(el);
|
||||
$el.data("shiny-intersection-observer-callback", onIntersect);
|
||||
$el.data("shiny-intersection-observer", io);
|
||||
}
|
||||
if (reportsTheme(el) && !$el.data("shiny-mutate-observer")) {
|
||||
const onMutate = sendOutputInfoFns.createObserverCallback(100, () => {
|
||||
if (reportsTheme(el)) doSendTheme(el);
|
||||
});
|
||||
const mo = new MutationObserver(() => onMutate());
|
||||
mo.observe(el, {
|
||||
attributes: true,
|
||||
attributeFilter: ["style", "class"]
|
||||
});
|
||||
$el.data("shiny-mutate-observer", mo);
|
||||
$el.data("shiny-mutate-observer-callback", onMutate);
|
||||
}
|
||||
}
|
||||
function doSendOutputInfo(initial = false) {
|
||||
const outputIds = /* @__PURE__ */ new Set();
|
||||
(0, import_jquery40.default)(".shiny-bound-output").each(function() {
|
||||
const el = this;
|
||||
const id = getIdFromEl(el);
|
||||
if (id) outputIds.add(id);
|
||||
ensureObservers(el);
|
||||
refreshOutputInfo(el, initial);
|
||||
});
|
||||
visibleOutputs.forEach((id) => {
|
||||
if (!outputIds.has(id)) {
|
||||
visibleOutputs.delete(id);
|
||||
setInput(".clientdata_output_" + id + "_hidden", true, initial);
|
||||
}
|
||||
handler.apply(this, [namespaceArr, handler, ...args]);
|
||||
};
|
||||
});
|
||||
}
|
||||
(0, import_jquery40.default)(window).resize(debounce(500, sendImageSizeFns.regular));
|
||||
const bs3classes = [
|
||||
"modal",
|
||||
"dropdown",
|
||||
"tab",
|
||||
"tooltip",
|
||||
"popover",
|
||||
"collapse"
|
||||
];
|
||||
import_jquery40.default.each(bs3classes, function(idx, classname) {
|
||||
(0, import_jquery40.default)(document.body).on(
|
||||
"shown.bs." + classname + ".sendImageSize",
|
||||
"*",
|
||||
filterEventsByNamespace("bs", sendImageSizeFns.regular)
|
||||
);
|
||||
(0, import_jquery40.default)(document.body).on(
|
||||
"shown.bs." + classname + ".sendOutputHiddenState hidden.bs." + classname + ".sendOutputHiddenState",
|
||||
"*",
|
||||
filterEventsByNamespace("bs", sendOutputHiddenState)
|
||||
);
|
||||
});
|
||||
(0, import_jquery40.default)(document.body).on("shown.sendImageSize", "*", sendImageSizeFns.regular);
|
||||
(0, import_jquery40.default)(document.body).on(
|
||||
"shown.sendOutputHiddenState hidden.sendOutputHiddenState",
|
||||
"*",
|
||||
sendOutputHiddenState
|
||||
);
|
||||
doSendOutputInfo(true);
|
||||
sendOutputInfoFns.setSendMethod(inputBatchSender, doSendOutputInfo);
|
||||
registerThemeRefreshSignals();
|
||||
initialValues[".clientdata_pixelratio"] = pixelRatio();
|
||||
(0, import_jquery40.default)(window).resize(function() {
|
||||
inputs.setInput(".clientdata_pixelratio", pixelRatio());
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
inst/www/shared/shiny.min.css
vendored
2
inst/www/shared/shiny.min.css
vendored
File diff suppressed because one or more lines are too long
40
inst/www/shared/shiny.min.js
vendored
40
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -463,10 +463,11 @@ textarea.textarea-autoresize.form-control {
|
||||
}
|
||||
}
|
||||
|
||||
// Add spacing between icon and label for actionButton()
|
||||
.action-button:not(.action-link) {
|
||||
// Add spacing between icon and label for action buttons and links (#4348)
|
||||
// Using margin instead of padding so the underline doesn't extend into the gap for links
|
||||
.action-button {
|
||||
.action-icon + .action-label {
|
||||
padding-left: 0.5ch;
|
||||
margin-left: 1ch;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@posit/shiny",
|
||||
"version": "1.11.1-alpha.9001",
|
||||
"version": "1.13.0-alpha.9000",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@posit/shiny",
|
||||
"version": "1.11.1-alpha.9001",
|
||||
"license": "GPL-3.0-only",
|
||||
"version": "1.13.0-alpha.9000",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/bootstrap": "5.2.x",
|
||||
"@types/bootstrap-datepicker": "1.10.0",
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"url": "git+https://github.com/rstudio/shiny.git"
|
||||
},
|
||||
"name": "@posit/shiny",
|
||||
"version": "1.12.1",
|
||||
"license": "GPL-3.0-only",
|
||||
"version": "1.13.0-alpha.9000",
|
||||
"license": "MIT",
|
||||
"main": "",
|
||||
"browser": "",
|
||||
"types": "srcts/types/extras/globalShiny.d.ts",
|
||||
@@ -63,9 +63,10 @@
|
||||
"bundle_shiny": "tsx srcts/build/shiny.ts",
|
||||
"bundle_external_libs": "tsx srcts/build/external_libs.ts",
|
||||
"bundle_extras": "tsx srcts/build/extras.ts",
|
||||
"checks": "npm run lint && npm run build_types && npm run coverage && npm run circular",
|
||||
"checks": "npm run lint && npm run build_types && npm run test_types && npm run coverage && npm run circular",
|
||||
"lint": "node --eval \"console.log('linting code...')\" && eslint 'srcts/src/**/*.ts' --fix",
|
||||
"build_types": "tsc -p tsconfig.json",
|
||||
"test_types": "tsx --test $(find srcts/src -path '*/__tests__/*.test.ts' -print)",
|
||||
"coverage_detailed": "npx --yes type-check --detail",
|
||||
"coverage": "type-coverage -p tsconfig.json --at-least 90",
|
||||
"circular": "npx --yes dpdm --transform ./srcts/src/index.ts",
|
||||
|
||||
@@ -39,17 +39,5 @@ declare global {
|
||||
): this;
|
||||
on(events: EvtPrefix<"mouseup">, handler: EvtFn<JQuery.MouseUpEvent>): this;
|
||||
on(events: EvtPrefix<"resize">, handler: EvtFn<JQuery.ResizeEvent>): this;
|
||||
|
||||
on(
|
||||
events: `shown.bs.${string}.sendImageSize`,
|
||||
selector: string,
|
||||
handler: (
|
||||
this: HTMLElement,
|
||||
e: JQuery.EventHandlerBase<HTMLElement, any>,
|
||||
// e: JQuery.Event & {
|
||||
// namespace: string;
|
||||
// }
|
||||
) => void,
|
||||
): this;
|
||||
}
|
||||
}
|
||||
|
||||
42
srcts/src/shiny/__tests__/sendOutputInfo.test.ts
Normal file
42
srcts/src/shiny/__tests__/sendOutputInfo.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { InputBatchSender } from "../../inputPolicies";
|
||||
import { SendOutputInfo } from "../sendOutputInfo";
|
||||
|
||||
void test("pending observer output info is flushed before the next input batch send", () => {
|
||||
const sentInputs: Array<{ [key: string]: unknown }> = [];
|
||||
const shinyapp = {
|
||||
taskQueue: {
|
||||
enqueue: () => {
|
||||
throw new Error("task queue should not be used in this test");
|
||||
},
|
||||
},
|
||||
sendInput: (values: { [key: string]: unknown }) => {
|
||||
sentInputs.push(values);
|
||||
},
|
||||
};
|
||||
const inputBatchSender = new InputBatchSender(shinyapp as never);
|
||||
const sendOutputInfo = new SendOutputInfo();
|
||||
|
||||
sendOutputInfo.setSendMethod(inputBatchSender, () => {
|
||||
/* no-op */
|
||||
});
|
||||
|
||||
const observerCallback = sendOutputInfo.createObserverCallback(100, () => {
|
||||
inputBatchSender.setInput(".clientdata_output_plot_width", 400, {
|
||||
priority: "immediate",
|
||||
});
|
||||
});
|
||||
|
||||
observerCallback();
|
||||
|
||||
inputBatchSender.setInput("user", 1, { priority: "event" });
|
||||
|
||||
assert.equal(sentInputs.length, 1);
|
||||
const expected: { [key: string]: unknown } = { user: 1 };
|
||||
|
||||
expected[".clientdata_output_plot_width"] = 400;
|
||||
|
||||
assert.deepEqual(sentInputs[0], expected);
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
} from "../inputPolicies";
|
||||
import type { EventPriority } from "../inputPolicies/inputPolicy";
|
||||
import { shinyAppBindOutput, shinyAppUnbindOutput } from "./initedMethods";
|
||||
import { sendImageSizeFns } from "./sendImageSize";
|
||||
import { sendOutputInfoFns } from "./sendOutputInfo";
|
||||
|
||||
type BindScope = HTMLElement | JQuery<HTMLElement>;
|
||||
|
||||
@@ -229,8 +229,6 @@ type BindInputsCtx = {
|
||||
inputsRate: InputRateDecorator;
|
||||
inputBindings: BindingRegistry<InputBinding>;
|
||||
outputBindings: BindingRegistry<OutputBinding>;
|
||||
sendOutputHiddenState: () => void;
|
||||
maybeAddThemeObserver: (el: HTMLElement) => void;
|
||||
initDeferredIframes: () => void;
|
||||
outputIsRecalculating: (id: string) => boolean;
|
||||
};
|
||||
@@ -318,12 +316,7 @@ function bindInputs(
|
||||
}
|
||||
|
||||
async function bindOutputs(
|
||||
{
|
||||
sendOutputHiddenState,
|
||||
maybeAddThemeObserver,
|
||||
outputBindings,
|
||||
outputIsRecalculating,
|
||||
}: BindInputsCtx,
|
||||
{ outputBindings, outputIsRecalculating }: BindInputsCtx,
|
||||
scope: BindScope = document.documentElement,
|
||||
): Promise<void> {
|
||||
const $scope = $(scope);
|
||||
@@ -355,12 +348,6 @@ async function bindOutputs(
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this element reports its CSS styles to getCurrentOutputInfo()
|
||||
// then it should have a MutationObserver() to resend CSS if its
|
||||
// style/class attributes change. This observer should already exist
|
||||
// for _static_ UI, but not yet for _dynamic_ UI
|
||||
maybeAddThemeObserver(el);
|
||||
|
||||
const bindingAdapter = new OutputBindingAdapter(el, binding);
|
||||
|
||||
await shinyAppBindOutput(id, bindingAdapter);
|
||||
@@ -383,8 +370,7 @@ async function bindOutputs(
|
||||
}
|
||||
|
||||
// Send later in case DOM layout isn't final yet.
|
||||
setTimeout(sendImageSizeFns.regular, 0);
|
||||
setTimeout(sendOutputHiddenState, 0);
|
||||
setTimeout(() => sendOutputInfoFns.regular(), 0);
|
||||
}
|
||||
|
||||
function unbindInputs(
|
||||
@@ -419,7 +405,6 @@ function unbindInputs(
|
||||
}
|
||||
}
|
||||
function unbindOutputs(
|
||||
{ sendOutputHiddenState }: BindInputsCtx,
|
||||
scope: BindScope = document.documentElement,
|
||||
includeSelf = false,
|
||||
) {
|
||||
@@ -443,6 +428,27 @@ function unbindOutputs(
|
||||
bindingsRegistry.removeBinding(id, "output");
|
||||
$el.removeClass("shiny-bound-output");
|
||||
$el.removeData("shiny-output-binding");
|
||||
|
||||
for (const prefix of [
|
||||
"shiny-resize-observer",
|
||||
"shiny-intersection-observer",
|
||||
"shiny-mutate-observer",
|
||||
]) {
|
||||
const observer = $el.data(prefix);
|
||||
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
$el.removeData(prefix);
|
||||
}
|
||||
|
||||
const callback = $el.data(prefix + "-callback") as
|
||||
| { cancel?: () => void }
|
||||
| undefined;
|
||||
|
||||
callback?.cancel?.();
|
||||
$el.removeData(prefix + "-callback");
|
||||
}
|
||||
|
||||
$el.trigger({
|
||||
type: "shiny:unbound",
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
@@ -452,8 +458,7 @@ function unbindOutputs(
|
||||
}
|
||||
|
||||
// Send later in case DOM layout isn't final yet.
|
||||
setTimeout(sendImageSizeFns.regular, 0);
|
||||
setTimeout(sendOutputHiddenState, 0);
|
||||
setTimeout(() => sendOutputInfoFns.regular(), 0);
|
||||
}
|
||||
|
||||
// (Named used before TS conversion)
|
||||
@@ -474,13 +479,9 @@ async function _bindAll(
|
||||
|
||||
return currentInputs;
|
||||
}
|
||||
function unbindAll(
|
||||
shinyCtx: BindInputsCtx,
|
||||
scope: BindScope,
|
||||
includeSelf = false,
|
||||
): void {
|
||||
function unbindAll(scope: BindScope, includeSelf = false): void {
|
||||
unbindInputs(scope, includeSelf);
|
||||
unbindOutputs(shinyCtx, scope, includeSelf);
|
||||
unbindOutputs(scope, includeSelf);
|
||||
}
|
||||
async function bindAll(
|
||||
shinyCtx: BindInputsCtx,
|
||||
|
||||
@@ -17,15 +17,14 @@ import {
|
||||
} from "../inputPolicies";
|
||||
import type { InputPolicyOpts } from "../inputPolicies/inputPolicy";
|
||||
import { addDefaultInputOpts } from "../inputPolicies/inputValidateDecorator";
|
||||
import { debounce, Debouncer } from "../time";
|
||||
import {
|
||||
$escape,
|
||||
compareVersion,
|
||||
getBoundingClientSizeBeforeZoom,
|
||||
getComputedLinkColor,
|
||||
getStyle,
|
||||
hasDefinedProperty,
|
||||
isShinyInDevMode,
|
||||
isVisible,
|
||||
mapValues,
|
||||
pixelRatio,
|
||||
} from "../utils";
|
||||
@@ -52,7 +51,7 @@ import {
|
||||
renderHtml,
|
||||
renderHtmlAsync,
|
||||
} from "./render";
|
||||
import { sendImageSizeFns } from "./sendImageSize";
|
||||
import { sendOutputInfoFns } from "./sendOutputInfo";
|
||||
import { addCustomMessageHandler, ShinyApp, type Handler } from "./shinyapp";
|
||||
import { registerNames as singletonsRegisterNames } from "./singletons";
|
||||
|
||||
@@ -220,8 +219,6 @@ class ShinyClass {
|
||||
return {
|
||||
inputs,
|
||||
inputsRate,
|
||||
sendOutputHiddenState,
|
||||
maybeAddThemeObserver,
|
||||
inputBindings,
|
||||
outputBindings,
|
||||
initDeferredIframes,
|
||||
@@ -234,7 +231,7 @@ class ShinyClass {
|
||||
await bindAll(shinyBindCtx(), scope);
|
||||
};
|
||||
this.unbindAll = function (scope: BindScope, includeSelf = false) {
|
||||
unbindAll(shinyBindCtx(), scope, includeSelf);
|
||||
unbindAll(scope, includeSelf);
|
||||
};
|
||||
|
||||
// Calls .initialize() for all of the input objects in all input bindings,
|
||||
@@ -262,12 +259,11 @@ class ShinyClass {
|
||||
}
|
||||
this.initializeInputs = initializeInputs;
|
||||
|
||||
function getIdFromEl(el: HTMLElement) {
|
||||
function getIdFromEl(el: HTMLElement): string | null {
|
||||
const $el = $(el);
|
||||
const bindingAdapter = $el.data("shiny-output-binding");
|
||||
|
||||
if (!bindingAdapter) return null;
|
||||
else return bindingAdapter.getId();
|
||||
return bindingAdapter ? bindingAdapter.getId() : null;
|
||||
}
|
||||
|
||||
// Initialize all input objects in the document, before binding
|
||||
@@ -285,327 +281,246 @@ class ShinyClass {
|
||||
(x) => x.value,
|
||||
);
|
||||
|
||||
// The server needs to know the size of each image and plot output element,
|
||||
// in case it is auto-sizing
|
||||
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
|
||||
function () {
|
||||
const id = getIdFromEl(this),
|
||||
rect = getBoundingClientSizeBeforeZoom(this);
|
||||
function setInput(name: string, value: unknown, initial = false): void {
|
||||
if (initial) {
|
||||
initialValues[name] = value;
|
||||
} else {
|
||||
inputs.setInput(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (rect.width !== 0 || rect.height !== 0) {
|
||||
initialValues[".clientdata_output_" + id + "_width"] = rect.width;
|
||||
initialValues[".clientdata_output_" + id + "_height"] = rect.height;
|
||||
}
|
||||
},
|
||||
);
|
||||
function doSendSize(el: HTMLElement, initial = false): void {
|
||||
const id = getIdFromEl(el);
|
||||
|
||||
function getComputedBgColor(
|
||||
el: HTMLElement | null,
|
||||
): string | null | undefined {
|
||||
if (!el) {
|
||||
// Top of document, can't recurse further
|
||||
return null;
|
||||
if (!id) return;
|
||||
|
||||
const rect = getBoundingClientSizeBeforeZoom(el);
|
||||
|
||||
if (rect.width !== 0 || rect.height !== 0) {
|
||||
setInput(".clientdata_output_" + id + "_width", rect.width, initial);
|
||||
setInput(".clientdata_output_" + id + "_height", rect.height, initial);
|
||||
}
|
||||
}
|
||||
|
||||
function doTriggerResize(el: HTMLElement): void {
|
||||
const $el = $(el),
|
||||
binding = $el.data("shiny-output-binding");
|
||||
|
||||
if (!binding) return;
|
||||
|
||||
$el.trigger({
|
||||
type: "shiny:visualchange",
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
visible: isVisible(el),
|
||||
binding: binding,
|
||||
});
|
||||
binding.onResize();
|
||||
}
|
||||
|
||||
function doSendTheme(el: HTMLElement, initial = false): void {
|
||||
if (el.classList.contains("shiny-output-error")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bgColor = getStyle(el, "background-color");
|
||||
function getComputedBgColor(
|
||||
el: HTMLElement | null,
|
||||
): string | null | undefined {
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!bgColor) return bgColor;
|
||||
const m = bgColor.match(
|
||||
/^rgba\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/,
|
||||
const bgColor = getStyle(el, "background-color");
|
||||
|
||||
if (!bgColor) return bgColor;
|
||||
const m = bgColor.match(
|
||||
/^rgba\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/,
|
||||
);
|
||||
|
||||
if (bgColor === "transparent" || (m && parseFloat(m[4]) === 0)) {
|
||||
const bgImage = getStyle(el, "background-image");
|
||||
|
||||
if (bgImage && bgImage !== "none") {
|
||||
return null;
|
||||
} else {
|
||||
return getComputedBgColor(el.parentElement);
|
||||
}
|
||||
}
|
||||
return bgColor;
|
||||
}
|
||||
|
||||
function getComputedFont(el: HTMLElement): {
|
||||
families: string[] | undefined;
|
||||
size: string | undefined;
|
||||
} {
|
||||
const fontFamily = getStyle(el, "font-family");
|
||||
const fontSize = getStyle(el, "font-size");
|
||||
|
||||
return {
|
||||
families: fontFamily?.replace(/"/g, "").split(", "),
|
||||
size: fontSize,
|
||||
};
|
||||
}
|
||||
|
||||
const id = getIdFromEl(el);
|
||||
|
||||
if (!id) return;
|
||||
|
||||
setInput(
|
||||
".clientdata_output_" + id + "_bg",
|
||||
getComputedBgColor(el),
|
||||
initial,
|
||||
);
|
||||
setInput(
|
||||
".clientdata_output_" + id + "_fg",
|
||||
getStyle(el, "color"),
|
||||
initial,
|
||||
);
|
||||
setInput(
|
||||
".clientdata_output_" + id + "_accent",
|
||||
getComputedLinkColor(el),
|
||||
initial,
|
||||
);
|
||||
setInput(
|
||||
".clientdata_output_" + id + "_font",
|
||||
getComputedFont(el),
|
||||
initial,
|
||||
);
|
||||
}
|
||||
|
||||
const visibleOutputs = new Set<string>();
|
||||
|
||||
function doSendHiddenState(el: HTMLElement, initial = false): void {
|
||||
const id = getIdFromEl(el);
|
||||
|
||||
if (!id) return;
|
||||
|
||||
const hidden = !isVisible(el);
|
||||
|
||||
if (hidden) {
|
||||
visibleOutputs.delete(id);
|
||||
} else {
|
||||
visibleOutputs.add(id);
|
||||
}
|
||||
setInput(".clientdata_output_" + id + "_hidden", hidden, initial);
|
||||
}
|
||||
|
||||
function reportsSize(el: HTMLElement): boolean {
|
||||
return (
|
||||
el.classList.contains("shiny-image-output") ||
|
||||
el.classList.contains("shiny-plot-output") ||
|
||||
el.classList.contains("shiny-report-size")
|
||||
);
|
||||
}
|
||||
|
||||
function reportsTheme(el: HTMLElement): boolean {
|
||||
return (
|
||||
el.classList.contains("shiny-image-output") ||
|
||||
el.classList.contains("shiny-plot-output") ||
|
||||
el.classList.contains("shiny-report-theme")
|
||||
);
|
||||
}
|
||||
|
||||
function refreshOutputInfo(el: HTMLElement, initial = false): void {
|
||||
if (!initial) doTriggerResize(el);
|
||||
doSendHiddenState(el, initial);
|
||||
if (reportsSize(el)) doSendSize(el, initial);
|
||||
if (reportsTheme(el)) doSendTheme(el, initial);
|
||||
}
|
||||
|
||||
function refreshThemeOutputs(initial = false): void {
|
||||
$(".shiny-bound-output").each(function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const el = this;
|
||||
|
||||
if (reportsTheme(el)) doSendTheme(el, initial);
|
||||
});
|
||||
}
|
||||
|
||||
function registerThemeRefreshSignals(): void {
|
||||
const scheduleThemeInfoRefresh = sendOutputInfoFns.createObserverCallback(
|
||||
100,
|
||||
() => refreshThemeOutputs(),
|
||||
);
|
||||
|
||||
if (bgColor === "transparent" || (m && parseFloat(m[4]) === 0)) {
|
||||
// No background color on this element. See if it has a background image.
|
||||
const bgImage = getStyle(el, "background-image");
|
||||
// Compatibility: window resize used to refresh theme-reporting output
|
||||
// info.
|
||||
$(window).resize(function () {
|
||||
scheduleThemeInfoRefresh();
|
||||
});
|
||||
|
||||
if (bgImage && bgImage !== "none") {
|
||||
// Failed to detect background color, since it has a background image
|
||||
return null;
|
||||
} else {
|
||||
// Recurse
|
||||
return getComputedBgColor(el.parentElement);
|
||||
}
|
||||
// Explicit API for code that changes surrounding theme context without
|
||||
// changing the output element itself.
|
||||
$(document).on("shiny:themechange", function () {
|
||||
scheduleThemeInfoRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
function ensureObservers(el: HTMLElement): void {
|
||||
const $el = $(el);
|
||||
|
||||
if (!$el.data("shiny-resize-observer")) {
|
||||
const onResize = sendOutputInfoFns.createObserverCallback(100, () =>
|
||||
refreshOutputInfo(el),
|
||||
);
|
||||
const ro = new ResizeObserver(() => onResize());
|
||||
|
||||
ro.observe(el);
|
||||
$el.data("shiny-resize-observer-callback", onResize);
|
||||
$el.data("shiny-resize-observer", ro);
|
||||
}
|
||||
|
||||
if (!$el.data("shiny-intersection-observer")) {
|
||||
const onIntersect = sendOutputInfoFns.createObserverCallback(100, () =>
|
||||
refreshOutputInfo(el),
|
||||
);
|
||||
const io = new IntersectionObserver(() => onIntersect());
|
||||
|
||||
io.observe(el);
|
||||
$el.data("shiny-intersection-observer-callback", onIntersect);
|
||||
$el.data("shiny-intersection-observer", io);
|
||||
}
|
||||
|
||||
if (reportsTheme(el) && !$el.data("shiny-mutate-observer")) {
|
||||
const onMutate = sendOutputInfoFns.createObserverCallback(100, () => {
|
||||
if (reportsTheme(el)) doSendTheme(el);
|
||||
});
|
||||
const mo = new MutationObserver(() => onMutate());
|
||||
|
||||
mo.observe(el, {
|
||||
attributes: true,
|
||||
attributeFilter: ["style", "class"],
|
||||
});
|
||||
|
||||
$el.data("shiny-mutate-observer", mo);
|
||||
$el.data("shiny-mutate-observer-callback", onMutate);
|
||||
}
|
||||
return bgColor;
|
||||
}
|
||||
|
||||
function getComputedFont(el: HTMLElement) {
|
||||
const fontFamily = getStyle(el, "font-family");
|
||||
const fontSize = getStyle(el, "font-size");
|
||||
function doSendOutputInfo(initial = false) {
|
||||
const outputIds = new Set<string>();
|
||||
|
||||
return {
|
||||
families: fontFamily?.replace(/"/g, "").split(", "),
|
||||
size: fontSize,
|
||||
};
|
||||
}
|
||||
|
||||
$(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
|
||||
function () {
|
||||
$(".shiny-bound-output").each(function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const el = this;
|
||||
const id = getIdFromEl(el);
|
||||
|
||||
initialValues[".clientdata_output_" + id + "_bg"] =
|
||||
getComputedBgColor(el);
|
||||
initialValues[".clientdata_output_" + id + "_fg"] = getStyle(
|
||||
el,
|
||||
"color",
|
||||
);
|
||||
initialValues[".clientdata_output_" + id + "_accent"] =
|
||||
getComputedLinkColor(el);
|
||||
initialValues[".clientdata_output_" + id + "_font"] =
|
||||
getComputedFont(el);
|
||||
maybeAddThemeObserver(el);
|
||||
},
|
||||
);
|
||||
if (id) outputIds.add(id);
|
||||
ensureObservers(el);
|
||||
|
||||
// Resend computed styles if *an output element's* class or style attribute changes.
|
||||
// This gives us some level of confidence that getCurrentOutputInfo() will be
|
||||
// properly invalidated if output container is mutated; but unfortunately,
|
||||
// we don't have a reasonable way to detect change in *inherited* styles
|
||||
// (other than session$setCurrentTheme())
|
||||
// https://github.com/rstudio/shiny/issues/3196
|
||||
// https://github.com/rstudio/shiny/issues/2998
|
||||
function maybeAddThemeObserver(el: HTMLElement): void {
|
||||
if (!window.MutationObserver) {
|
||||
return; // IE10 and lower
|
||||
}
|
||||
refreshOutputInfo(el, initial);
|
||||
});
|
||||
|
||||
const cl = el.classList;
|
||||
const reportTheme =
|
||||
cl.contains("shiny-image-output") ||
|
||||
cl.contains("shiny-plot-output") ||
|
||||
cl.contains("shiny-report-theme");
|
||||
|
||||
if (!reportTheme) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $el = $(el);
|
||||
|
||||
if ($el.data("shiny-theme-observer")) {
|
||||
return; // i.e., observer is already observing
|
||||
}
|
||||
|
||||
const observerCallback = new Debouncer(null, () => doSendTheme(el), 100);
|
||||
const observer = new MutationObserver(() =>
|
||||
observerCallback.normalCall(),
|
||||
);
|
||||
const config = { attributes: true, attributeFilter: ["style", "class"] };
|
||||
|
||||
observer.observe(el, config);
|
||||
$el.data("shiny-theme-observer", observer);
|
||||
}
|
||||
|
||||
function doSendTheme(el: HTMLElement): void {
|
||||
// Sending theme info on error isn't necessary (it'd add an unnecessary additional round-trip)
|
||||
if (el.classList.contains("shiny-output-error")) {
|
||||
return;
|
||||
}
|
||||
const id = getIdFromEl(el);
|
||||
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_bg",
|
||||
getComputedBgColor(el),
|
||||
);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_fg",
|
||||
getStyle(el, "color"),
|
||||
);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_accent",
|
||||
getComputedLinkColor(el),
|
||||
);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_font",
|
||||
getComputedFont(el),
|
||||
);
|
||||
}
|
||||
|
||||
function doSendImageSize() {
|
||||
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
|
||||
function () {
|
||||
const id = getIdFromEl(this),
|
||||
rect = getBoundingClientSizeBeforeZoom(this);
|
||||
|
||||
if (rect.width !== 0 || rect.height !== 0) {
|
||||
inputs.setInput(".clientdata_output_" + id + "_width", rect.width);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_height",
|
||||
rect.height,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
$(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
|
||||
function () {
|
||||
doSendTheme(this);
|
||||
},
|
||||
);
|
||||
|
||||
$(".shiny-bound-output").each(function () {
|
||||
const $this = $(this),
|
||||
binding = $this.data("shiny-output-binding");
|
||||
|
||||
$this.trigger({
|
||||
type: "shiny:visualchange",
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
visible: !isHidden(this),
|
||||
binding: binding,
|
||||
});
|
||||
binding.onResize();
|
||||
visibleOutputs.forEach((id) => {
|
||||
if (!outputIds.has(id)) {
|
||||
visibleOutputs.delete(id);
|
||||
setInput(".clientdata_output_" + id + "_hidden", true, initial);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sendImageSizeFns.setImageSend(inputBatchSender, doSendImageSize);
|
||||
|
||||
// Return true if the object or one of its ancestors in the DOM tree has
|
||||
// style='display:none'; otherwise return false.
|
||||
function isHidden(obj: HTMLElement | null): boolean {
|
||||
// null means we've hit the top of the tree. If width or height is
|
||||
// non-zero, then we know that no ancestor has display:none.
|
||||
if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) {
|
||||
return false;
|
||||
} else if (getStyle(obj, "display") === "none") {
|
||||
return true;
|
||||
} else {
|
||||
return isHidden(obj.parentNode as HTMLElement | null);
|
||||
}
|
||||
}
|
||||
let lastKnownVisibleOutputs: { [key: string]: boolean } = {};
|
||||
// Set initial state of outputs to hidden, if needed
|
||||
|
||||
$(".shiny-bound-output").each(function () {
|
||||
const id = getIdFromEl(this);
|
||||
|
||||
if (isHidden(this)) {
|
||||
initialValues[".clientdata_output_" + id + "_hidden"] = true;
|
||||
} else {
|
||||
lastKnownVisibleOutputs[id] = true;
|
||||
initialValues[".clientdata_output_" + id + "_hidden"] = false;
|
||||
}
|
||||
});
|
||||
// Send update when hidden state changes
|
||||
function doSendOutputHiddenState() {
|
||||
const visibleOutputs: { [key: string]: boolean } = {};
|
||||
|
||||
$(".shiny-bound-output").each(function () {
|
||||
const id = getIdFromEl(this);
|
||||
|
||||
delete lastKnownVisibleOutputs[id];
|
||||
// Assume that the object is hidden when width and height are 0
|
||||
const hidden = isHidden(this),
|
||||
evt = {
|
||||
type: "shiny:visualchange",
|
||||
visible: !hidden,
|
||||
};
|
||||
|
||||
if (hidden) {
|
||||
inputs.setInput(".clientdata_output_" + id + "_hidden", true);
|
||||
} else {
|
||||
visibleOutputs[id] = true;
|
||||
inputs.setInput(".clientdata_output_" + id + "_hidden", false);
|
||||
}
|
||||
const $this = $(this);
|
||||
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
evt.binding = $this.data("shiny-output-binding");
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
$this.trigger(evt);
|
||||
});
|
||||
// Anything left in lastKnownVisibleOutputs is orphaned
|
||||
for (const name in lastKnownVisibleOutputs) {
|
||||
if (hasDefinedProperty(lastKnownVisibleOutputs, name))
|
||||
inputs.setInput(".clientdata_output_" + name + "_hidden", true);
|
||||
}
|
||||
// Update the visible outputs for next time
|
||||
lastKnownVisibleOutputs = visibleOutputs;
|
||||
}
|
||||
// sendOutputHiddenState gets called each time DOM elements are shown or
|
||||
// hidden. This can be in the hundreds or thousands of times at startup.
|
||||
// We'll debounce it, so that we do the actual work once per tick.
|
||||
const sendOutputHiddenStateDebouncer = new Debouncer(
|
||||
null,
|
||||
doSendOutputHiddenState,
|
||||
0,
|
||||
);
|
||||
|
||||
function sendOutputHiddenState() {
|
||||
sendOutputHiddenStateDebouncer.normalCall();
|
||||
}
|
||||
// We need to make sure doSendOutputHiddenState actually gets called before
|
||||
// the inputBatchSender sends data to the server. The lastChanceCallback
|
||||
// here does that - if the debouncer has a pending call, flush it.
|
||||
inputBatchSender.lastChanceCallback.push(function () {
|
||||
if (sendOutputHiddenStateDebouncer.isPending())
|
||||
sendOutputHiddenStateDebouncer.immediateCall();
|
||||
});
|
||||
|
||||
// Given a namespace and a handler function, return a function that invokes
|
||||
// the handler only when e's namespace matches. For example, if the
|
||||
// namespace is "bs", it would match when e.namespace is "bs" or "bs.tab".
|
||||
// If the namespace is "bs.tab", it would match for "bs.tab", but not "bs".
|
||||
function filterEventsByNamespace(
|
||||
namespace: string,
|
||||
handler: (...handlerArgs: any[]) => void,
|
||||
...args: any[]
|
||||
) {
|
||||
const namespaceArr = namespace.split(".");
|
||||
|
||||
return function (this: HTMLElement, e: JQuery.TriggeredEvent) {
|
||||
const eventNamespace = e.namespace?.split(".") ?? [];
|
||||
|
||||
// If any of the namespace strings aren't present in this event, quit.
|
||||
for (let i = 0; i < namespaceArr.length; i++) {
|
||||
if (eventNamespace.indexOf(namespaceArr[i]) === -1) return;
|
||||
}
|
||||
|
||||
handler.apply(this, [namespaceArr, handler, ...args]);
|
||||
};
|
||||
}
|
||||
|
||||
// The size of each image may change either because the browser window was
|
||||
// resized, or because a tab was shown/hidden (hidden elements report size
|
||||
// of 0x0). It's OK to over-report sizes because the input pipeline will
|
||||
// filter out values that haven't changed.
|
||||
$(window).resize(debounce(500, sendImageSizeFns.regular));
|
||||
// Need to register callbacks for each Bootstrap 3 class.
|
||||
const bs3classes = [
|
||||
"modal",
|
||||
"dropdown",
|
||||
"tab",
|
||||
"tooltip",
|
||||
"popover",
|
||||
"collapse",
|
||||
];
|
||||
|
||||
$.each(bs3classes, function (idx, classname) {
|
||||
$(document.body).on(
|
||||
"shown.bs." + classname + ".sendImageSize",
|
||||
"*",
|
||||
filterEventsByNamespace("bs", sendImageSizeFns.regular),
|
||||
);
|
||||
$(document.body).on(
|
||||
"shown.bs." +
|
||||
classname +
|
||||
".sendOutputHiddenState " +
|
||||
"hidden.bs." +
|
||||
classname +
|
||||
".sendOutputHiddenState",
|
||||
"*",
|
||||
filterEventsByNamespace("bs", sendOutputHiddenState),
|
||||
);
|
||||
});
|
||||
|
||||
// This is needed for Bootstrap 2 compatibility and for non-Bootstrap
|
||||
// related shown/hidden events (like conditionalPanel)
|
||||
$(document.body).on("shown.sendImageSize", "*", sendImageSizeFns.regular);
|
||||
$(document.body).on(
|
||||
"shown.sendOutputHiddenState hidden.sendOutputHiddenState",
|
||||
"*",
|
||||
sendOutputHiddenState,
|
||||
);
|
||||
doSendOutputInfo(true);
|
||||
sendOutputInfoFns.setSendMethod(inputBatchSender, doSendOutputInfo);
|
||||
registerThemeRefreshSignals();
|
||||
|
||||
// Send initial pixel ratio, and update it if it changes
|
||||
initialValues[".clientdata_pixelratio"] = pixelRatio();
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
shinyInitializeInputs,
|
||||
shinyUnbindAll,
|
||||
} from "./initedMethods";
|
||||
import { sendImageSizeFns } from "./sendImageSize";
|
||||
import { sendOutputInfoFns } from "./sendOutputInfo";
|
||||
|
||||
import type { WherePosition } from "./singletons";
|
||||
import { renderHtml as singletonsRenderHtml } from "./singletons";
|
||||
@@ -267,7 +267,7 @@ function addStylesheetsAndRestyle(links: HTMLLinkElement[]): void {
|
||||
// should have been applied synchronously.
|
||||
oldStyle.remove();
|
||||
removeSheet(oldSheet);
|
||||
sendImageSizeFns.transitioned();
|
||||
sendOutputInfoFns.transitioned();
|
||||
};
|
||||
xhr.send();
|
||||
};
|
||||
@@ -327,7 +327,7 @@ function addStylesheetsAndRestyle(links: HTMLLinkElement[]): void {
|
||||
// base64-encoded and inlined into the href. We also add a dummy DOM
|
||||
// element that the CSS applies to. The dummy CSS includes a
|
||||
// transition, and when the `transitionend` event happens, we call
|
||||
// sendImageSizeFns.transitioned() and remove the old sheet. We also remove the
|
||||
// sendOutputInfoFns.transitioned() and remove the old sheet. We also remove the
|
||||
// dummy DOM element and dummy CSS content.
|
||||
//
|
||||
// The reason this works is because (we assume) that if multiple
|
||||
@@ -337,7 +337,7 @@ function addStylesheetsAndRestyle(links: HTMLLinkElement[]): void {
|
||||
//
|
||||
// Because it is common for multiple stylesheets to arrive close
|
||||
// together, but not on exactly the same tick, we call
|
||||
// sendImageSizeFns.transitioned(), which is debounced. Otherwise, it can result in
|
||||
// sendOutputInfoFns.transitioned(), which is debounced. Otherwise, it can result in
|
||||
// the same plot being redrawn multiple times with different
|
||||
// styling.
|
||||
$link.attr("onload", () => {
|
||||
@@ -350,7 +350,7 @@ function addStylesheetsAndRestyle(links: HTMLLinkElement[]): void {
|
||||
$dummyEl.one("transitionend", () => {
|
||||
$dummyEl.remove();
|
||||
removeSheet(oldSheet);
|
||||
sendImageSizeFns.transitioned();
|
||||
sendOutputInfoFns.transitioned();
|
||||
});
|
||||
$(document.body).append($dummyEl);
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { InputBatchSender } from "../inputPolicies";
|
||||
import { debounce, Debouncer } from "../time";
|
||||
|
||||
class SendImageSize {
|
||||
// This function gets defined in initShiny() and 'hoisted' so it can be reused
|
||||
// (to send CSS info) inside of Shiny.renderDependencies()
|
||||
regular!: () => void;
|
||||
transitioned!: () => void;
|
||||
|
||||
setImageSend(
|
||||
inputBatchSender: InputBatchSender,
|
||||
doSendImageSize: () => void,
|
||||
): Debouncer<typeof doSendImageSize> {
|
||||
const sendImageSizeDebouncer = new Debouncer(null, doSendImageSize, 0);
|
||||
|
||||
this.regular = function () {
|
||||
sendImageSizeDebouncer.normalCall();
|
||||
};
|
||||
|
||||
// Make sure sendImageSize actually gets called before the inputBatchSender
|
||||
// sends data to the server.
|
||||
inputBatchSender.lastChanceCallback.push(function () {
|
||||
if (sendImageSizeDebouncer.isPending())
|
||||
sendImageSizeDebouncer.immediateCall();
|
||||
});
|
||||
|
||||
// A version of sendImageSize which debounces for longer.
|
||||
this.transitioned = debounce(200, this.regular);
|
||||
|
||||
return sendImageSizeDebouncer;
|
||||
}
|
||||
}
|
||||
|
||||
const sendImageSizeFns = new SendImageSize();
|
||||
|
||||
export { sendImageSizeFns };
|
||||
77
srcts/src/shiny/sendOutputInfo.ts
Normal file
77
srcts/src/shiny/sendOutputInfo.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { InputBatchSender } from "../inputPolicies";
|
||||
import { debounce, Debouncer } from "../time";
|
||||
|
||||
type FlushableObserverCallback = (() => void) & {
|
||||
cancel: () => void;
|
||||
flush: () => void;
|
||||
isPending: () => boolean;
|
||||
};
|
||||
|
||||
class SendOutputInfo {
|
||||
regular!: () => void;
|
||||
transitioned!: () => void;
|
||||
#pendingObserverCallbacks = new Set<FlushableObserverCallback>();
|
||||
|
||||
setSendMethod(
|
||||
inputBatchSender: InputBatchSender,
|
||||
doSendOutputInfo: () => void,
|
||||
): Debouncer<typeof doSendOutputInfo> {
|
||||
const sendOutputInfoDebouncer = new Debouncer(null, doSendOutputInfo, 0);
|
||||
|
||||
this.regular = function () {
|
||||
sendOutputInfoDebouncer.normalCall();
|
||||
};
|
||||
|
||||
inputBatchSender.lastChanceCallback.push(() => {
|
||||
this.#pendingObserverCallbacks.forEach((callback) => callback.flush());
|
||||
|
||||
if (sendOutputInfoDebouncer.isPending())
|
||||
sendOutputInfoDebouncer.immediateCall();
|
||||
});
|
||||
|
||||
this.transitioned = debounce(200, this.regular);
|
||||
|
||||
return sendOutputInfoDebouncer;
|
||||
}
|
||||
|
||||
createObserverCallback(
|
||||
delayMs: number,
|
||||
callback: () => void,
|
||||
): FlushableObserverCallback {
|
||||
const debouncer = new Debouncer(
|
||||
null,
|
||||
() => {
|
||||
this.#pendingObserverCallbacks.delete(observerCallback);
|
||||
callback();
|
||||
},
|
||||
delayMs,
|
||||
);
|
||||
|
||||
const observerCallback: FlushableObserverCallback = Object.assign(
|
||||
() => {
|
||||
this.#pendingObserverCallbacks.add(observerCallback);
|
||||
debouncer.normalCall();
|
||||
},
|
||||
{
|
||||
cancel: () => {
|
||||
this.#pendingObserverCallbacks.delete(observerCallback);
|
||||
debouncer.cancel();
|
||||
},
|
||||
flush: () => {
|
||||
this.#pendingObserverCallbacks.delete(observerCallback);
|
||||
if (debouncer.isPending()) {
|
||||
debouncer.immediateCall();
|
||||
}
|
||||
},
|
||||
isPending: () => debouncer.isPending(),
|
||||
},
|
||||
);
|
||||
|
||||
return observerCallback;
|
||||
}
|
||||
}
|
||||
|
||||
const sendOutputInfoFns = new SendOutputInfo();
|
||||
|
||||
export { SendOutputInfo, sendOutputInfoFns };
|
||||
export type { FlushableObserverCallback };
|
||||
18
srcts/src/time/__tests__/debounce.test.ts
Normal file
18
srcts/src/time/__tests__/debounce.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { debounce } from "../debounce";
|
||||
|
||||
void test("debounce can cancel a pending callback before it fires", async () => {
|
||||
let calls = 0;
|
||||
const debounced = debounce(10, () => {
|
||||
calls += 1;
|
||||
});
|
||||
|
||||
debounced();
|
||||
debounced.cancel();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
@@ -39,6 +39,10 @@ class Debouncer<X extends AnyVoidFunction> implements InputRatePolicy<X> {
|
||||
this.args = args;
|
||||
this.$invoke();
|
||||
}
|
||||
cancel(): void {
|
||||
this.$clearTimer();
|
||||
this.args = null;
|
||||
}
|
||||
isPending(): boolean {
|
||||
return this.timerId !== null;
|
||||
}
|
||||
@@ -70,15 +74,21 @@ class Debouncer<X extends AnyVoidFunction> implements InputRatePolicy<X> {
|
||||
// 900ms intervals will result in a single execution
|
||||
// of the underlying function, 1000ms after the 17th
|
||||
// call.
|
||||
type DebouncedFunction<T extends (...args: unknown[]) => void> = ((
|
||||
...args: Parameters<T>
|
||||
) => void) & {
|
||||
cancel: () => void;
|
||||
};
|
||||
|
||||
function debounce<T extends (...args: unknown[]) => void>(
|
||||
threshold: number | undefined,
|
||||
func: T,
|
||||
): (...args: Parameters<T>) => void {
|
||||
): DebouncedFunction<T> {
|
||||
let timerId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Do not alter `function()` into an arrow function.
|
||||
// The `this` context needs to be dynamically bound
|
||||
return function thisFunc(...args: Parameters<T>) {
|
||||
const debounced = function thisFunc(...args: Parameters<T>) {
|
||||
if (timerId !== null) {
|
||||
clearTimeout(timerId);
|
||||
timerId = null;
|
||||
@@ -92,6 +102,16 @@ function debounce<T extends (...args: unknown[]) => void>(
|
||||
func.apply(thisFunc, args);
|
||||
}, threshold);
|
||||
};
|
||||
|
||||
debounced.cancel = function () {
|
||||
if (timerId !== null) {
|
||||
clearTimeout(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
}
|
||||
|
||||
export { debounce, Debouncer };
|
||||
export type { DebouncedFunction };
|
||||
|
||||
@@ -59,6 +59,16 @@ function getStyle(el: Element, styleProp: string): string | undefined {
|
||||
return x;
|
||||
}
|
||||
|
||||
function isVisible(el: HTMLElement): boolean {
|
||||
if (el.offsetWidth !== 0 || el.offsetHeight !== 0) {
|
||||
return true;
|
||||
}
|
||||
if (getStyle(el, "display") === "none") {
|
||||
return false;
|
||||
}
|
||||
return el.parentElement ? isVisible(el.parentElement) : true;
|
||||
}
|
||||
|
||||
// Convert a number to a string with leading zeros
|
||||
function padZeros(n: number, digits: number): string {
|
||||
let str = n.toString();
|
||||
@@ -421,6 +431,7 @@ export {
|
||||
isBS3,
|
||||
isnan,
|
||||
isShinyInDevMode,
|
||||
isVisible,
|
||||
makeResizeFilter,
|
||||
mapValues,
|
||||
mergeSort,
|
||||
|
||||
1
srcts/types/src/events/jQueryEvents.d.ts
vendored
1
srcts/types/src/events/jQueryEvents.d.ts
vendored
@@ -13,7 +13,6 @@ declare global {
|
||||
on(events: EvtPrefix<"mousedown2">, handler: EvtFn<JQuery.MouseDownEvent>): this;
|
||||
on(events: EvtPrefix<"mouseup">, handler: EvtFn<JQuery.MouseUpEvent>): this;
|
||||
on(events: EvtPrefix<"resize">, handler: EvtFn<JQuery.ResizeEvent>): this;
|
||||
on(events: `shown.bs.${string}.sendImageSize`, selector: string, handler: (this: HTMLElement, e: JQuery.EventHandlerBase<HTMLElement, any>) => void): this;
|
||||
}
|
||||
}
|
||||
export {};
|
||||
|
||||
4
srcts/types/src/shiny/bind.d.ts
vendored
4
srcts/types/src/shiny/bind.d.ts
vendored
@@ -7,8 +7,6 @@ type BindInputsCtx = {
|
||||
inputsRate: InputRateDecorator;
|
||||
inputBindings: BindingRegistry<InputBinding>;
|
||||
outputBindings: BindingRegistry<OutputBinding>;
|
||||
sendOutputHiddenState: () => void;
|
||||
maybeAddThemeObserver: (el: HTMLElement) => void;
|
||||
initDeferredIframes: () => void;
|
||||
outputIsRecalculating: (id: string) => boolean;
|
||||
};
|
||||
@@ -23,7 +21,7 @@ declare function bindInputs(shinyCtx: BindInputsCtx, scope?: BindScope): {
|
||||
};
|
||||
};
|
||||
declare function _bindAll(shinyCtx: BindInputsCtx, scope: BindScope): Promise<ReturnType<typeof bindInputs>>;
|
||||
declare function unbindAll(shinyCtx: BindInputsCtx, scope: BindScope, includeSelf?: boolean): void;
|
||||
declare function unbindAll(scope: BindScope, includeSelf?: boolean): void;
|
||||
declare function bindAll(shinyCtx: BindInputsCtx, scope: BindScope): Promise<void>;
|
||||
export { _bindAll, bindAll, unbindAll };
|
||||
export type { BindInputsCtx, BindScope };
|
||||
|
||||
9
srcts/types/src/shiny/sendImageSize.d.ts
vendored
9
srcts/types/src/shiny/sendImageSize.d.ts
vendored
@@ -1,9 +0,0 @@
|
||||
import type { InputBatchSender } from "../inputPolicies";
|
||||
import { Debouncer } from "../time";
|
||||
declare class SendImageSize {
|
||||
regular: () => void;
|
||||
transitioned: () => void;
|
||||
setImageSend(inputBatchSender: InputBatchSender, doSendImageSize: () => void): Debouncer<typeof doSendImageSize>;
|
||||
}
|
||||
declare const sendImageSizeFns: SendImageSize;
|
||||
export { sendImageSizeFns };
|
||||
17
srcts/types/src/shiny/sendOutputInfo.d.ts
vendored
Normal file
17
srcts/types/src/shiny/sendOutputInfo.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { InputBatchSender } from "../inputPolicies";
|
||||
import { Debouncer } from "../time";
|
||||
type FlushableObserverCallback = (() => void) & {
|
||||
cancel: () => void;
|
||||
flush: () => void;
|
||||
isPending: () => boolean;
|
||||
};
|
||||
declare class SendOutputInfo {
|
||||
#private;
|
||||
regular: () => void;
|
||||
transitioned: () => void;
|
||||
setSendMethod(inputBatchSender: InputBatchSender, doSendOutputInfo: () => void): Debouncer<typeof doSendOutputInfo>;
|
||||
createObserverCallback(delayMs: number, callback: () => void): FlushableObserverCallback;
|
||||
}
|
||||
declare const sendOutputInfoFns: SendOutputInfo;
|
||||
export { SendOutputInfo, sendOutputInfoFns };
|
||||
export type { FlushableObserverCallback };
|
||||
7
srcts/types/src/time/debounce.d.ts
vendored
7
srcts/types/src/time/debounce.d.ts
vendored
@@ -10,9 +10,14 @@ declare class Debouncer<X extends AnyVoidFunction> implements InputRatePolicy<X>
|
||||
constructor(target: InputPolicy | null, func: X, delayMs: number | undefined);
|
||||
normalCall(...args: Parameters<X>): void;
|
||||
immediateCall(...args: Parameters<X>): void;
|
||||
cancel(): void;
|
||||
isPending(): boolean;
|
||||
$clearTimer(): void;
|
||||
$invoke(): void;
|
||||
}
|
||||
declare function debounce<T extends (...args: unknown[]) => void>(threshold: number | undefined, func: T): (...args: Parameters<T>) => void;
|
||||
type DebouncedFunction<T extends (...args: unknown[]) => void> = ((...args: Parameters<T>) => void) & {
|
||||
cancel: () => void;
|
||||
};
|
||||
declare function debounce<T extends (...args: unknown[]) => void>(threshold: number | undefined, func: T): DebouncedFunction<T>;
|
||||
export { debounce, Debouncer };
|
||||
export type { DebouncedFunction };
|
||||
|
||||
3
srcts/types/src/utils/index.d.ts
vendored
3
srcts/types/src/utils/index.d.ts
vendored
@@ -5,6 +5,7 @@ declare function escapeHTML(str: string): string;
|
||||
declare function randomId(): string;
|
||||
declare function strToBool(str: string): boolean | undefined;
|
||||
declare function getStyle(el: Element, styleProp: string): string | undefined;
|
||||
declare function isVisible(el: HTMLElement): boolean;
|
||||
declare function padZeros(n: number, digits: number): string;
|
||||
declare function roundSignif(x: number, digits?: number): number;
|
||||
declare function parseDate(dateString: string): Date;
|
||||
@@ -34,4 +35,4 @@ declare function getComputedLinkColor(el: HTMLElement): string;
|
||||
declare function isBS3(): boolean;
|
||||
declare function toLowerCase<T extends string>(str: T): Lowercase<T>;
|
||||
declare function isShinyInDevMode(): boolean;
|
||||
export { $escape, _equal, asArray, compareVersion, equal, escapeHTML, formatDateUTC, getBoundingClientSizeBeforeZoom, getComputedLinkColor, getStyle, hasDefinedProperty, hasOwnProperty, isBS3, isnan, isShinyInDevMode, makeResizeFilter, mapValues, mergeSort, padZeros, parseDate, pixelRatio, randomId, roundSignif, scopeExprToFunc, strToBool, toLowerCase, updateLabel, };
|
||||
export { $escape, _equal, asArray, compareVersion, equal, escapeHTML, formatDateUTC, getBoundingClientSizeBeforeZoom, getComputedLinkColor, getStyle, hasDefinedProperty, hasOwnProperty, isBS3, isnan, isShinyInDevMode, isVisible, makeResizeFilter, mapValues, mergeSort, padZeros, parseDate, pixelRatio, randomId, roundSignif, scopeExprToFunc, strToBool, toLowerCase, updateLabel, };
|
||||
|
||||
@@ -3,19 +3,26 @@
|
||||
Code
|
||||
actionButton("foo", "Click me")
|
||||
Output
|
||||
<button id="foo" type="button" class="btn btn-default action-button">
|
||||
<span class="action-label">Click me</span>
|
||||
</button>
|
||||
<button id="foo" type="button" class="btn btn-default action-button"><span class="action-label">Click me</span></button>
|
||||
|
||||
---
|
||||
|
||||
Code
|
||||
actionButton("foo", "Click me", icon = icon("star"))
|
||||
Output
|
||||
<button id="foo" type="button" class="btn btn-default action-button">
|
||||
<span class="action-icon">
|
||||
<i class="far fa-star" role="presentation" aria-label="star icon"></i>
|
||||
</span>
|
||||
<span class="action-label">Click me</span>
|
||||
</button>
|
||||
<button id="foo" type="button" class="btn btn-default action-button"><span class="action-icon"><i class="far fa-star" role="presentation" aria-label="star icon"></i></span><span class="action-label">Click me</span></button>
|
||||
|
||||
# actionLink uses .noWS to prevent underline rendering issues
|
||||
|
||||
Code
|
||||
actionLink("foo", "Click me")
|
||||
Output
|
||||
<a id="foo" href="#" class="action-button action-link"><span class="action-label">Click me</span></a>
|
||||
|
||||
---
|
||||
|
||||
Code
|
||||
actionLink("foo", "Click me", icon = icon("star"))
|
||||
Output
|
||||
<a id="foo" href="#" class="action-button action-link"><span class="action-icon"><i class="far fa-star" role="presentation" aria-label="star icon"></i></span><span class="action-label">Click me</span></a>
|
||||
|
||||
|
||||
@@ -1,111 +1,103 @@
|
||||
# integration tests
|
||||
|
||||
Code
|
||||
df
|
||||
df_integration_slim
|
||||
Output
|
||||
num call loc
|
||||
1 68 A [test-stacks.R#3]
|
||||
2 67 B [test-stacks.R#7]
|
||||
3 66 <reactive:C> [test-stacks.R#11]
|
||||
4 44 C
|
||||
5 43 renderTable [test-stacks.R#18]
|
||||
6 42 func
|
||||
7 41 force
|
||||
8 40 withVisible
|
||||
9 39 withCallingHandlers
|
||||
10 38 domain$wrapSync
|
||||
11 37 promises::with_promise_domain
|
||||
12 36 captureStackTraces
|
||||
13 32 tryCatch
|
||||
14 31 do
|
||||
15 30 hybrid_chain
|
||||
16 29 renderFunc
|
||||
17 28 renderTable({ C() }, server = FALSE)
|
||||
18 10 isolate
|
||||
19 9 withCallingHandlers [test-stacks.R#16]
|
||||
20 8 domain$wrapSync
|
||||
21 7 promises::with_promise_domain
|
||||
22 6 captureStackTraces
|
||||
23 2 tryCatch
|
||||
24 1 try
|
||||
25 0 causeError [test-stacks.R#14]
|
||||
1 70 A [test-stacks.R#3]
|
||||
2 69 B [test-stacks.R#7]
|
||||
3 68 <reactive:C> [test-stacks.R#11]
|
||||
4 46 C
|
||||
5 45 renderTable [test-stacks.R#18]
|
||||
6 44 func
|
||||
7 28 renderTable({ C() }, server = FALSE)
|
||||
8 10 isolate
|
||||
9 9 withCallingHandlers [test-stacks.R#16]
|
||||
10 8 domain$wrapSync
|
||||
11 7 promises::with_promise_domain
|
||||
12 6 captureStackTraces
|
||||
13 2 tryCatch
|
||||
14 1 try
|
||||
15 0 causeError [test-stacks.R#14]
|
||||
|
||||
---
|
||||
|
||||
Code
|
||||
df
|
||||
df_integration_full
|
||||
Output
|
||||
num call loc
|
||||
1 71 h
|
||||
2 70 .handleSimpleError
|
||||
3 69 stop
|
||||
4 68 A [test-stacks.R#3]
|
||||
5 67 B [test-stacks.R#7]
|
||||
6 66 <reactive:C> [test-stacks.R#11]
|
||||
7 65 ..stacktraceon..
|
||||
8 64 .func
|
||||
9 63 withVisible
|
||||
10 62 withCallingHandlers
|
||||
11 61 contextFunc
|
||||
12 60 env$runWith
|
||||
13 59 withCallingHandlers
|
||||
14 58 domain$wrapSync
|
||||
15 57 promises::with_promise_domain
|
||||
16 56 captureStackTraces
|
||||
17 55 force
|
||||
18 54 with_otel_span_context
|
||||
19 53 force
|
||||
20 52 domain$wrapSync
|
||||
21 51 promises::with_promise_domain
|
||||
22 50 withReactiveDomain
|
||||
23 49 domain$wrapSync
|
||||
24 48 promises::with_promise_domain
|
||||
25 47 ctx$run
|
||||
26 46 self$.updateValue
|
||||
27 45 ..stacktraceoff..
|
||||
28 44 C
|
||||
29 43 renderTable [test-stacks.R#18]
|
||||
30 42 func
|
||||
31 41 force
|
||||
32 40 withVisible
|
||||
33 39 withCallingHandlers
|
||||
34 38 domain$wrapSync
|
||||
35 37 promises::with_promise_domain
|
||||
36 36 captureStackTraces
|
||||
37 35 doTryCatch
|
||||
38 34 tryCatchOne
|
||||
39 33 tryCatchList
|
||||
40 32 tryCatch
|
||||
41 31 do
|
||||
42 30 hybrid_chain
|
||||
43 29 renderFunc
|
||||
44 28 renderTable({ C() }, server = FALSE)
|
||||
45 27 ..stacktraceon.. [test-stacks.R#17]
|
||||
46 26 contextFunc
|
||||
47 25 env$runWith
|
||||
48 24 withCallingHandlers
|
||||
49 23 domain$wrapSync
|
||||
50 22 promises::with_promise_domain
|
||||
51 21 captureStackTraces
|
||||
52 20 force
|
||||
53 19 with_otel_span_context
|
||||
54 18 force
|
||||
55 17 domain$wrapSync
|
||||
56 16 promises::with_promise_domain
|
||||
57 15 withReactiveDomain
|
||||
58 14 domain$wrapSync
|
||||
59 13 promises::with_promise_domain
|
||||
60 12 ctx$run
|
||||
61 11 ..stacktraceoff..
|
||||
62 10 isolate
|
||||
63 9 withCallingHandlers [test-stacks.R#16]
|
||||
64 8 domain$wrapSync
|
||||
65 7 promises::with_promise_domain
|
||||
66 6 captureStackTraces
|
||||
67 5 doTryCatch [test-stacks.R#15]
|
||||
68 4 tryCatchOne
|
||||
69 3 tryCatchList
|
||||
70 2 tryCatch
|
||||
71 1 try
|
||||
72 0 causeError [test-stacks.R#14]
|
||||
1 73 h
|
||||
2 72 .handleSimpleError
|
||||
3 71 stop
|
||||
4 70 A [test-stacks.R#3]
|
||||
5 69 B [test-stacks.R#7]
|
||||
6 68 <reactive:C> [test-stacks.R#11]
|
||||
7 67 ..stacktraceon..
|
||||
8 66 .func
|
||||
9 65 withVisible
|
||||
10 64 withCallingHandlers
|
||||
11 63 contextFunc
|
||||
12 62 env$runWith
|
||||
13 61 withCallingHandlers
|
||||
14 60 domain$wrapSync
|
||||
15 59 promises::with_promise_domain
|
||||
16 58 captureStackTraces
|
||||
17 57 force
|
||||
18 56 with_otel_span_context
|
||||
19 55 force
|
||||
20 54 domain$wrapSync
|
||||
21 53 promises::with_promise_domain
|
||||
22 52 withReactiveDomain
|
||||
23 51 domain$wrapSync
|
||||
24 50 promises::with_promise_domain
|
||||
25 49 ctx$run
|
||||
26 48 self$.updateValue
|
||||
27 47 ..stacktraceoff..
|
||||
28 46 C
|
||||
29 45 renderTable [test-stacks.R#18]
|
||||
30 44 func
|
||||
31 43 ..stacktraceon..
|
||||
32 42 force
|
||||
33 41 withVisible
|
||||
34 40 withCallingHandlers
|
||||
35 39 domain$wrapSync
|
||||
36 38 promises::with_promise_domain
|
||||
37 37 captureStackTraces
|
||||
38 36 doTryCatch
|
||||
39 35 tryCatchOne
|
||||
40 34 tryCatchList
|
||||
41 33 tryCatch
|
||||
42 32 do
|
||||
43 31 hybrid_chain
|
||||
44 30 renderFunc
|
||||
45 29 ..stacktraceoff..
|
||||
46 28 renderTable({ C() }, server = FALSE)
|
||||
47 27 ..stacktraceon.. [test-stacks.R#17]
|
||||
48 26 contextFunc
|
||||
49 25 env$runWith
|
||||
50 24 withCallingHandlers
|
||||
51 23 domain$wrapSync
|
||||
52 22 promises::with_promise_domain
|
||||
53 21 captureStackTraces
|
||||
54 20 force
|
||||
55 19 with_otel_span_context
|
||||
56 18 force
|
||||
57 17 domain$wrapSync
|
||||
58 16 promises::with_promise_domain
|
||||
59 15 withReactiveDomain
|
||||
60 14 domain$wrapSync
|
||||
61 13 promises::with_promise_domain
|
||||
62 12 ctx$run
|
||||
63 11 ..stacktraceoff..
|
||||
64 10 isolate
|
||||
65 9 withCallingHandlers [test-stacks.R#16]
|
||||
66 8 domain$wrapSync
|
||||
67 7 promises::with_promise_domain
|
||||
68 6 captureStackTraces
|
||||
69 5 doTryCatch [test-stacks.R#15]
|
||||
70 4 tryCatchOne
|
||||
71 3 tryCatchList
|
||||
72 2 tryCatch
|
||||
73 1 try
|
||||
74 0 causeError [test-stacks.R#14]
|
||||
|
||||
|
||||
@@ -353,7 +353,7 @@
|
||||
<div class="tabbable">
|
||||
<ul class="nav nav-tabs" data-tabsetid="4785">
|
||||
<li>
|
||||
<a href="#tab-4785-1" data-toggle="tab" data-bs-toggle="tab"></a>
|
||||
<a href="#tab-4785-1" data-toggle="tab" data-bs-toggle="tab" disabled></a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="#tab-4785-2" data-toggle="tab" data-bs-toggle="tab" data-value="A">A</a>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
skip_if_shiny_otel_tracer_is_enabled <- function() {
|
||||
if (shiny_otel_tracer()$is_enabled()) {
|
||||
skip("Skipping stack trace tests when OpenTelemetry is already enabled")
|
||||
}
|
||||
}
|
||||
|
||||
# Helper function to create a mock otel span
|
||||
create_mock_otel_span <- function(name = "test_span") {
|
||||
structure(
|
||||
|
||||
118
tests/testthat/helper-stacks.R
Normal file
118
tests/testthat/helper-stacks.R
Normal file
@@ -0,0 +1,118 @@
|
||||
#' @details `extractStackTrace` takes a list of calls (e.g. as returned
|
||||
#' from `conditionStackTrace(cond)`) and returns a data frame with one
|
||||
#' row for each stack frame and the columns `num` (stack frame number),
|
||||
#' `call` (a function name or similar), and `loc` (source file path
|
||||
#' and line number, if available). It was deprecated after shiny 1.0.5 because
|
||||
#' it doesn't support deep stack traces.
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
extractStackTrace <- function(calls,
|
||||
full = get_devmode_option("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
srcrefs <- getSrcRefs(calls)
|
||||
if (offset) {
|
||||
# Offset calls vs. srcrefs by 1 to make them more intuitive.
|
||||
# E.g. for "foo [bar.R:10]", line 10 of bar.R will be part of
|
||||
# the definition of foo().
|
||||
srcrefs <- c(utils::tail(srcrefs, -1), list(NULL))
|
||||
}
|
||||
calls <- setSrcRefs(calls, srcrefs)
|
||||
|
||||
callnames <- getCallNames(calls)
|
||||
|
||||
# Hide and show parts of the callstack based on ..stacktrace(on|off)..
|
||||
if (full) {
|
||||
toShow <- rep.int(TRUE, length(calls))
|
||||
} else {
|
||||
# Remove stop(), .handleSimpleError(), and h() calls from the end of
|
||||
# the calls--they don't add any helpful information. But only remove
|
||||
# the last *contiguous* block of them, and then, only if they are the
|
||||
# last thing in the calls list.
|
||||
hideable <- callnames %in% c("stop", ".handleSimpleError", "h")
|
||||
# What's the last that *didn't* match stop/.handleSimpleError/h?
|
||||
lastGoodCall <- max(which(!hideable))
|
||||
toRemove <- length(calls) - lastGoodCall
|
||||
# But don't remove more than 5 levels--that's an indication we might
|
||||
# have gotten it wrong, I guess
|
||||
if (toRemove > 0 && toRemove < 5) {
|
||||
calls <- utils::head(calls, -toRemove)
|
||||
callnames <- utils::head(callnames, -toRemove)
|
||||
}
|
||||
|
||||
toShow <- stripStackTraces(list(callnames))[[1]]
|
||||
|
||||
toShow <-
|
||||
toShow &
|
||||
# doTryCatch, tryCatchOne, and tryCatchList are not informative--they're
|
||||
# just internals for tryCatch
|
||||
!(callnames %in% c("doTryCatch", "tryCatchOne", "tryCatchList")) &
|
||||
# doWithOneRestart and withOneRestart are not informative--they're
|
||||
# just internals for withRestarts
|
||||
!(callnames %in% c("withOneRestart", "doWithOneRestart"))
|
||||
}
|
||||
calls <- calls[toShow]
|
||||
|
||||
|
||||
calls <- rev(calls) # Show in traceback() order
|
||||
index <- rev(which(toShow))
|
||||
width <- floor(log10(max(index))) + 1
|
||||
|
||||
data.frame(
|
||||
num = index,
|
||||
call = getCallNames(calls),
|
||||
loc = getLocs(calls),
|
||||
# category = getCallCategories(calls),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
cleanLocs <- function(locs) {
|
||||
locs[!grepl("test-stacks\\.R", locs, perl = TRUE)] <- ""
|
||||
# sub("^.*#", "", locs)
|
||||
locs
|
||||
}
|
||||
|
||||
dumpTests <- function(df) {
|
||||
print(bquote({
|
||||
expect_equal(df$num, .(df$num))
|
||||
expect_equal(df$call, .(df$call))
|
||||
expect_equal(nzchar(df$loc), .(nzchar(df$loc)))
|
||||
}))
|
||||
}
|
||||
|
||||
# Helper: run a render function whose body throws an error, capture the
|
||||
# stack trace, apply fence-based filtering, and return the filtered data
|
||||
# frame. The render function body should call a function that calls stop().
|
||||
# `needs_session` indicates whether the render function requires
|
||||
# shinysession/name parameters (TRUE for markRenderFunction-based renders
|
||||
# like renderPlot and renderPrint, FALSE for createRenderFunction-based
|
||||
# renders like renderText/renderTable/renderUI/renderImage which can be
|
||||
# called with no args).
|
||||
captureFilteredRenderTrace <- function(render_fn, needs_session = TRUE) {
|
||||
session <- MockShinySession$new()
|
||||
on.exit(if (!session$isClosed()) session$close())
|
||||
|
||||
res <- try({
|
||||
captureStackTraces({
|
||||
isolate({
|
||||
withReactiveDomain(session, {
|
||||
if (needs_session) {
|
||||
render_fn(shinysession = session, name = "testoutput")
|
||||
} else {
|
||||
render_fn()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
silent = TRUE)
|
||||
|
||||
cond <- attr(res, "condition", exact = TRUE)
|
||||
stopifnot(!is.null(cond))
|
||||
stopifnot(!is.null(conditionStackTrace(cond)))
|
||||
|
||||
suppressMessages(
|
||||
extractStackTrace(conditionStackTrace(cond), full = FALSE)
|
||||
)
|
||||
}
|
||||
@@ -94,3 +94,30 @@ test_that("Action button allows icon customization", {
|
||||
expect_equal(as_character(btn2), as_character(btn3))
|
||||
expect_equal(as_character(btn3), as_character(btn4))
|
||||
})
|
||||
|
||||
test_that("actionLink uses .noWS to prevent underline rendering issues", {
|
||||
# actionLink should generate compact HTML without whitespace between tags
|
||||
# This prevents the underline from extending beyond the visible text
|
||||
|
||||
# Test without icon
|
||||
link <- actionLink("test_link", "Click me")
|
||||
link_html <- as.character(link)
|
||||
|
||||
# Verify no newlines/whitespace between closing > and opening <span
|
||||
expect_false(
|
||||
grepl(">\n\\s+<span", link_html),
|
||||
info = "actionLink should not have whitespace between tags"
|
||||
)
|
||||
|
||||
# Test with icon
|
||||
link_icon <- actionLink("test_link2", "Click me", icon = icon("star"))
|
||||
link_icon_html <- as.character(link_icon)
|
||||
|
||||
# Should also have no whitespace between icon span and label span
|
||||
expect_false(
|
||||
grepl(">\n\\s+<span", link_icon_html),
|
||||
info = "actionLink with icon should not have whitespace between tags"
|
||||
)
|
||||
expect_snapshot(actionLink("foo", "Click me"))
|
||||
expect_snapshot(actionLink("foo", "Click me", icon = icon("star")))
|
||||
})
|
||||
|
||||
@@ -1140,7 +1140,7 @@ test_that("Custom render functions that call installExprFunction", {
|
||||
|
||||
|
||||
test_that("cacheWriteHook and cacheReadHook for render functions", {
|
||||
testthat::skip_if(shiny_otel_tracer()$is_enabled(), "Skipping stack trace tests when OpenTelemetry is already enabled")
|
||||
skip_if_shiny_otel_tracer_is_enabled()
|
||||
|
||||
write_hook_n <- 0
|
||||
read_hook_n <- 0
|
||||
|
||||
@@ -127,6 +127,7 @@ test_that("ExtendedTask respects reactive_update level otel collection", {
|
||||
})
|
||||
|
||||
test_that("ExtendedTask creates span only when is_recording_otel is TRUE", {
|
||||
skip_if_not_installed("otelsdk")
|
||||
# Test that span is only created when otel is enabled
|
||||
withr::local_options(list(shiny.otel.collect = "reactivity"))
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ test_that("reactive bindCache labels are created", {
|
||||
})
|
||||
|
||||
test_that("ExtendedTask otel labels are created", {
|
||||
skip_if_not_installed("otelsdk")
|
||||
# Record everything
|
||||
localOtelCollect("all")
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
skip_if_not_installed("ggplot2")
|
||||
library(ggplot2)
|
||||
|
||||
# Sort a list by the names of its keys
|
||||
|
||||
@@ -14,6 +14,140 @@ test_that("can access reactive values directly", {
|
||||
expect_equal(y(), 4)
|
||||
})
|
||||
|
||||
describe("srcfilealias in reactive labels", {
|
||||
# When a #line directive specifies a path that differs from the srcfilecopy
|
||||
# filename, R's parser wraps the srcfile in a srcfilealias whose $lines is
|
||||
# NULL. This is exactly what happens in sourceUTF8() when the normalized path
|
||||
# differs from the original.
|
||||
parse_as_srcfilealias <- function(user_code) {
|
||||
code <- c('#line 1 "/absolute/path/to/app.R"', user_code)
|
||||
src <- base::srcfilecopy("app.R", code, isFile = TRUE)
|
||||
exprs <- parse(text = code, keep.source = TRUE, srcfile = src)
|
||||
list(code = code, exprs = exprs, srcrefs = attr(exprs, "srcref"))
|
||||
}
|
||||
|
||||
it("getSrcfileLines() resolves lines from srcfilealias", {
|
||||
parsed <- parse_as_srcfilealias("my_val <- reactiveVal(1)")
|
||||
|
||||
srcref <- parsed$srcrefs[[1]]
|
||||
srcfile <- attr(srcref, "srcfile", exact = TRUE)
|
||||
|
||||
expect_s3_class(srcfile, "srcfilealias")
|
||||
expect_null(srcfile$lines)
|
||||
|
||||
result <- getSrcfileLines(srcfile, srcref)
|
||||
expect_false(is.null(result$lines))
|
||||
expect_equal(result$lines, parsed$code)
|
||||
expect_match(result$lines[result$line_num], "my_val <- reactiveVal")
|
||||
})
|
||||
|
||||
it("getSrcfileLines() works with regular srcfile", {
|
||||
code <- c("x <- 1", "y <- 2")
|
||||
src <- base::srcfilecopy("test.R", code, isFile = TRUE)
|
||||
exprs <- parse(text = code, keep.source = TRUE, srcfile = src)
|
||||
|
||||
srcref <- attr(exprs, "srcref")[[1]]
|
||||
srcfile <- attr(srcref, "srcfile", exact = TRUE)
|
||||
|
||||
expect_false(inherits(srcfile, "srcfilealias"))
|
||||
|
||||
result <- getSrcfileLines(srcfile, srcref)
|
||||
expect_equal(result$lines, code)
|
||||
expect_equal(result$line_num, 1L)
|
||||
})
|
||||
|
||||
it("rassignSrcrefToLabel() extracts label from srcfilealias", {
|
||||
parsed <- parse_as_srcfilealias("my_val <- reactiveVal(1)")
|
||||
srcref <- parsed$srcrefs[[1]]
|
||||
|
||||
label <- rassignSrcrefToLabel(srcref, defaultLabel = "fallback")
|
||||
expect_equal(label, "my_val")
|
||||
})
|
||||
|
||||
it("rexprSrcrefToLabel() extracts label from srcfilealias", {
|
||||
parsed <- parse_as_srcfilealias("my_r <- reactive({ 1 + 1 })")
|
||||
|
||||
# rexprSrcrefToLabel() expects the srcref of the reactive body (the { }),
|
||||
# not the entire assignment. This mirrors how exprToLabel() calls it with
|
||||
# the srcref from the body of the expression created by installExprFunction.
|
||||
assign_expr <- parsed$exprs[[1]]
|
||||
reactive_body <- assign_expr[[3]][[2]] # reactive( <body> )
|
||||
body_srcrefs <- attr(reactive_body, "srcref")
|
||||
srcref <- body_srcrefs[[1]]
|
||||
|
||||
label <- rexprSrcrefToLabel(srcref, defaultLabel = "fallback", fnName = "reactive")
|
||||
expect_equal(label, "my_r")
|
||||
})
|
||||
})
|
||||
|
||||
test_that("sourceUTF8() auto-labels reactives despite srcfilealias", {
|
||||
# sourceUTF8() uses normalizePath() in its #line directive but the original
|
||||
# path for srcfilecopy. When these differ (e.g. macOS /tmp -> /private/tmp),
|
||||
# R creates a srcfilealias whose $lines is NULL. When they match (e.g.
|
||||
# Ubuntu), the #line directive still remaps line numbers. getSrcfileLines()
|
||||
# handles both cases by using srcref[7] (the pre-remap line number).
|
||||
tmp <- tempfile(fileext = ".R")
|
||||
on.exit(unlink(tmp), add = TRUE)
|
||||
|
||||
reactiveConsole(TRUE)
|
||||
on.exit(reactiveConsole(FALSE), add = TRUE)
|
||||
|
||||
writeLines(c(
|
||||
"my_val <- reactiveVal(1)",
|
||||
"my_react <- reactive({ my_val() + 1 })"
|
||||
), tmp)
|
||||
|
||||
env <- new.env(parent = globalenv())
|
||||
sourceUTF8(tmp, envir = env)
|
||||
|
||||
# reactiveVal label (uses rassignSrcrefToLabel)
|
||||
rv_impl <- attr(env$my_val, ".impl", exact = TRUE)
|
||||
expect_equal(
|
||||
rv_impl$.__enclos_env__$private$label,
|
||||
"my_val"
|
||||
)
|
||||
|
||||
# reactive label (uses rexprSrcrefToLabel via exprToLabel)
|
||||
r_observable <- attr(env$my_react, "observable", exact = TRUE)
|
||||
expect_equal(as.character(r_observable$.label), "my_react")
|
||||
})
|
||||
|
||||
describe("srcfilealias filename selection", {
|
||||
parse_as_srcfilealias <- function(user_code, alias_path = "/absolute/path/to/app.R") {
|
||||
code <- c(sprintf('#line 1 "%s"', alias_path), user_code)
|
||||
src <- base::srcfilecopy("app.R", code, isFile = TRUE)
|
||||
exprs <- parse(text = code, keep.source = TRUE, srcfile = src)
|
||||
list(code = code, exprs = exprs, srcrefs = attr(exprs, "srcref"))
|
||||
}
|
||||
|
||||
it("getSrcfileFilename() prefers original unless package file", {
|
||||
lib <- normalizePath(.libPaths()[[1]], winslash = "/", mustWork = FALSE)
|
||||
pkg_path <- file.path(lib, "pkg", "R", "foo.R")
|
||||
|
||||
parsed_pkg <- parse_as_srcfilealias("x <- 1", alias_path = pkg_path)
|
||||
srcref_pkg <- parsed_pkg$srcrefs[[1]]
|
||||
srcfile_pkg <- attr(srcref_pkg, "srcfile", exact = TRUE)
|
||||
expect_equal(getSrcfileFilename(srcfile_pkg), pkg_path)
|
||||
|
||||
parsed_user <- parse_as_srcfilealias("y <- 2", alias_path = "/tmp/user.R")
|
||||
srcref_user <- parsed_user$srcrefs[[1]]
|
||||
srcfile_user <- attr(srcref_user, "srcfile", exact = TRUE)
|
||||
expect_equal(getSrcfileFilename(srcfile_user), "app.R")
|
||||
})
|
||||
})
|
||||
|
||||
test_that("isPackageFile() uses path-boundary matching", {
|
||||
lib <- normalizePath(.libPaths()[[1]], winslash = "/", mustWork = FALSE)
|
||||
|
||||
# A path like "{lib}Extra/foo.R" shares the prefix but is NOT inside the lib
|
||||
fake_path <- paste0(lib, "Extra/foo.R")
|
||||
expect_false(isPackageFile(fake_path))
|
||||
|
||||
# A path actually inside the library SHOULD match
|
||||
real_path <- file.path(lib, "pkg", "R", "foo.R")
|
||||
expect_true(isPackageFile(real_path))
|
||||
})
|
||||
|
||||
test_that("errors in throttled/debounced reactives are catchable", {
|
||||
reactiveConsole(TRUE)
|
||||
on.exit(reactiveConsole(FALSE))
|
||||
|
||||
@@ -240,6 +240,7 @@ test_that("stack trace stripping works", {
|
||||
})
|
||||
|
||||
test_that("coro async generator deep stack count is low", {
|
||||
skip_if_not_installed("coro")
|
||||
gen <- coro::async_generator(function() {
|
||||
for (i in 1:50) {
|
||||
await(coro::async_sleep(0.001))
|
||||
|
||||
@@ -32,97 +32,6 @@ causeError <- function(full) {
|
||||
df
|
||||
}
|
||||
|
||||
#' @details `extractStackTrace` takes a list of calls (e.g. as returned
|
||||
#' from `conditionStackTrace(cond)`) and returns a data frame with one
|
||||
#' row for each stack frame and the columns `num` (stack frame number),
|
||||
#' `call` (a function name or similar), and `loc` (source file path
|
||||
#' and line number, if available). It was deprecated after shiny 1.0.5 because
|
||||
#' it doesn't support deep stack traces.
|
||||
#' @rdname stacktrace
|
||||
#' @export
|
||||
extractStackTrace <- function(calls,
|
||||
full = get_devmode_option("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
srcrefs <- getSrcRefs(calls)
|
||||
if (offset) {
|
||||
# Offset calls vs. srcrefs by 1 to make them more intuitive.
|
||||
# E.g. for "foo [bar.R:10]", line 10 of bar.R will be part of
|
||||
# the definition of foo().
|
||||
srcrefs <- c(utils::tail(srcrefs, -1), list(NULL))
|
||||
}
|
||||
calls <- setSrcRefs(calls, srcrefs)
|
||||
|
||||
callnames <- getCallNames(calls)
|
||||
|
||||
# Hide and show parts of the callstack based on ..stacktrace(on|off)..
|
||||
if (full) {
|
||||
toShow <- rep.int(TRUE, length(calls))
|
||||
} else {
|
||||
# Remove stop(), .handleSimpleError(), and h() calls from the end of
|
||||
# the calls--they don't add any helpful information. But only remove
|
||||
# the last *contiguous* block of them, and then, only if they are the
|
||||
# last thing in the calls list.
|
||||
hideable <- callnames %in% c("stop", ".handleSimpleError", "h")
|
||||
# What's the last that *didn't* match stop/.handleSimpleError/h?
|
||||
lastGoodCall <- max(which(!hideable))
|
||||
toRemove <- length(calls) - lastGoodCall
|
||||
# But don't remove more than 5 levels--that's an indication we might
|
||||
# have gotten it wrong, I guess
|
||||
if (toRemove > 0 && toRemove < 5) {
|
||||
calls <- utils::head(calls, -toRemove)
|
||||
callnames <- utils::head(callnames, -toRemove)
|
||||
}
|
||||
|
||||
# This uses a ref-counting scheme. It might make sense to switch this
|
||||
# to a toggling scheme, so the most recent ..stacktrace(on|off)..
|
||||
# directive wins, regardless of what came before it.
|
||||
# Also explicitly remove ..stacktraceon.. because it can appear with
|
||||
# score > 0 but still should never be shown.
|
||||
score <- rep.int(0, length(callnames))
|
||||
score[callnames == "..stacktraceoff.."] <- -1
|
||||
score[callnames == "..stacktraceon.."] <- 1
|
||||
toShow <- (1 + cumsum(score)) > 0 & !(callnames %in% c("..stacktraceon..", "..stacktraceoff..", "..stacktracefloor.."))
|
||||
|
||||
toShow <-
|
||||
toShow &
|
||||
# doTryCatch, tryCatchOne, and tryCatchList are not informative--they're
|
||||
# just internals for tryCatch
|
||||
!(callnames %in% c("doTryCatch", "tryCatchOne", "tryCatchList")) &
|
||||
# doWithOneRestart and withOneRestart are not informative--they're
|
||||
# just internals for withRestarts
|
||||
!(callnames %in% c("withOneRestart", "doWithOneRestart"))
|
||||
}
|
||||
calls <- calls[toShow]
|
||||
|
||||
|
||||
calls <- rev(calls) # Show in traceback() order
|
||||
index <- rev(which(toShow))
|
||||
width <- floor(log10(max(index))) + 1
|
||||
|
||||
data.frame(
|
||||
num = index,
|
||||
call = getCallNames(calls),
|
||||
loc = getLocs(calls),
|
||||
# category = getCallCategories(calls),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
cleanLocs <- function(locs) {
|
||||
locs[!grepl("test-stacks\\.R", locs, perl = TRUE)] <- ""
|
||||
# sub("^.*#", "", locs)
|
||||
locs
|
||||
}
|
||||
|
||||
dumpTests <- function(df) {
|
||||
print(bquote({
|
||||
expect_equal(df$num, .(df$num))
|
||||
expect_equal(df$call, .(df$call))
|
||||
expect_equal(nzchar(df$loc), .(nzchar(df$loc)))
|
||||
}))
|
||||
}
|
||||
|
||||
test_that("integration tests", {
|
||||
if (shiny_otel_tracer()$is_enabled()) {
|
||||
announce_snapshot_file(name = "stacks.md")
|
||||
@@ -139,15 +48,15 @@ test_that("integration tests", {
|
||||
# problems on CRAN.
|
||||
skip_on_cran()
|
||||
|
||||
df <- causeError(full = FALSE)
|
||||
# dumpTests(df)
|
||||
df_integration_slim <- causeError(full = FALSE)
|
||||
# dumpTests(df_integration_slim)
|
||||
|
||||
expect_snapshot(df)
|
||||
expect_snapshot(df_integration_slim)
|
||||
|
||||
df <- causeError(full = TRUE)
|
||||
df_integration_full <- causeError(full = TRUE)
|
||||
|
||||
expect_snapshot(df)
|
||||
# dumpTests(df)
|
||||
expect_snapshot(df_integration_full)
|
||||
# dumpTests(df_integration_full)
|
||||
})
|
||||
|
||||
test_that("shiny.error", {
|
||||
@@ -272,3 +181,170 @@ test_that("observeEvent is not overly stripped (#4162)", {
|
||||
expect_match(st_str, "A__", all = FALSE)
|
||||
expect_match(st_str, "B__", all = FALSE)
|
||||
})
|
||||
|
||||
test_that("renderPlot stack trace fences hide internal rendering pipeline (#4357)", {
|
||||
skip_on_cran()
|
||||
|
||||
skip_if_shiny_otel_tracer_is_enabled()
|
||||
|
||||
userFunc <- function() {
|
||||
stop("test error in renderPlot")
|
||||
}
|
||||
|
||||
df <- captureFilteredRenderTrace(renderPlot({ userFunc() }))
|
||||
|
||||
expect_true("userFunc" %in% df$call)
|
||||
|
||||
# Internal rendering pipeline frames should NOT appear in the filtered
|
||||
# stack trace. These are Shiny internals between the stack trace fences
|
||||
# that currently leak through due to missing fences.
|
||||
internal_render_frames <- c(
|
||||
"drawPlot",
|
||||
"drawReactive",
|
||||
"renderFunc",
|
||||
"startPNG"
|
||||
)
|
||||
|
||||
leaked <- df$call[df$call %in% internal_render_frames]
|
||||
expect_length(leaked, 0)
|
||||
})
|
||||
|
||||
test_that("renderPrint stack trace fences hide internal rendering pipeline (#4357)", {
|
||||
skip_on_cran()
|
||||
|
||||
skip_if_shiny_otel_tracer_is_enabled()
|
||||
|
||||
userFunc <- function() {
|
||||
stop("test error in renderPrint")
|
||||
}
|
||||
|
||||
df <- captureFilteredRenderTrace(renderPrint({ userFunc() }))
|
||||
|
||||
expect_true("userFunc" %in% df$call)
|
||||
|
||||
internal_render_frames <- c("renderFunc")
|
||||
leaked <- df$call[df$call %in% internal_render_frames]
|
||||
expect_length(leaked, 0)
|
||||
})
|
||||
|
||||
test_that("renderText stack trace fences hide internal rendering pipeline (#4357)", {
|
||||
skip_on_cran()
|
||||
|
||||
skip_if_shiny_otel_tracer_is_enabled()
|
||||
|
||||
userFunc <- function() {
|
||||
stop("test error in renderText")
|
||||
}
|
||||
|
||||
df <- captureFilteredRenderTrace(renderText({ userFunc() }), needs_session = FALSE)
|
||||
|
||||
expect_true("userFunc" %in% df$call)
|
||||
|
||||
internal_render_frames <- c("renderFunc")
|
||||
leaked <- df$call[df$call %in% internal_render_frames]
|
||||
expect_length(leaked, 0)
|
||||
})
|
||||
|
||||
test_that("renderUI stack trace fences hide internal rendering pipeline (#4357)", {
|
||||
skip_on_cran()
|
||||
|
||||
skip_if_shiny_otel_tracer_is_enabled()
|
||||
|
||||
userFunc <- function() {
|
||||
stop("test error in renderUI")
|
||||
}
|
||||
|
||||
df <- captureFilteredRenderTrace(renderUI({ userFunc() }), needs_session = FALSE)
|
||||
|
||||
expect_true("userFunc" %in% df$call)
|
||||
|
||||
internal_render_frames <- c("renderFunc")
|
||||
leaked <- df$call[df$call %in% internal_render_frames]
|
||||
expect_length(leaked, 0)
|
||||
})
|
||||
|
||||
test_that("renderTable stack trace fences hide internal rendering pipeline (#4357)", {
|
||||
skip_on_cran()
|
||||
|
||||
skip_if_shiny_otel_tracer_is_enabled()
|
||||
|
||||
userFunc <- function() {
|
||||
stop("test error in renderTable")
|
||||
}
|
||||
|
||||
df <- captureFilteredRenderTrace(
|
||||
renderTable({ userFunc() }, server = FALSE),
|
||||
needs_session = FALSE
|
||||
)
|
||||
|
||||
expect_true("userFunc" %in% df$call)
|
||||
|
||||
internal_render_frames <- c("renderFunc")
|
||||
leaked <- df$call[df$call %in% internal_render_frames]
|
||||
expect_length(leaked, 0)
|
||||
})
|
||||
|
||||
test_that("renderImage stack trace fences hide internal rendering pipeline (#4357)", {
|
||||
skip_on_cran()
|
||||
|
||||
skip_if_shiny_otel_tracer_is_enabled()
|
||||
|
||||
userFunc <- function() {
|
||||
stop("test error in renderImage")
|
||||
}
|
||||
|
||||
df <- captureFilteredRenderTrace(
|
||||
renderImage({ userFunc() }, deleteFile = FALSE),
|
||||
needs_session = FALSE
|
||||
)
|
||||
|
||||
expect_true("userFunc" %in% df$call)
|
||||
|
||||
internal_render_frames <- c("renderFunc")
|
||||
leaked <- df$call[df$call %in% internal_render_frames]
|
||||
expect_length(leaked, 0)
|
||||
})
|
||||
|
||||
test_that("legacyRenderDataTable stack trace fences hide internal rendering pipeline (#4357)", {
|
||||
skip_on_cran()
|
||||
|
||||
skip_if_shiny_otel_tracer_is_enabled()
|
||||
|
||||
userFunc <- function() {
|
||||
stop("test error in renderDataTable")
|
||||
}
|
||||
|
||||
df <- captureFilteredRenderTrace(
|
||||
legacyRenderDataTable({ userFunc() })
|
||||
)
|
||||
|
||||
expect_true("userFunc" %in% df$call)
|
||||
|
||||
internal_render_frames <- c("renderFunc")
|
||||
leaked <- df$call[df$call %in% internal_render_frames]
|
||||
expect_length(leaked, 0)
|
||||
})
|
||||
|
||||
test_that("markRenderFunction preserves user frames outside reactive domain", {
|
||||
skip_on_cran()
|
||||
|
||||
skip_if_shiny_otel_tracer_is_enabled()
|
||||
|
||||
# htmlwidgets-style: exprToFunction + markRenderFunction, no ..stacktraceon..
|
||||
renderWidgetLike <- function(expr, env = parent.frame(), quoted = FALSE) {
|
||||
if (!quoted) expr <- substitute(expr)
|
||||
func <- exprToFunction(expr, env, TRUE)
|
||||
renderFunc <- function() { func() }
|
||||
markRenderFunction(textOutput, renderFunc)
|
||||
}
|
||||
|
||||
userFunc <- function() stop("boom")
|
||||
render_fn <- renderWidgetLike({ userFunc() })
|
||||
|
||||
res <- try(captureStackTraces({ render_fn() }), silent = TRUE)
|
||||
cond <- attr(res, "condition", exact = TRUE)
|
||||
df <- extractStackTrace(conditionStackTrace(cond), full = FALSE)
|
||||
|
||||
expect_true("userFunc" %in% df$call)
|
||||
})
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ test_that("runTests works with a dir app that calls modules and uses testServer"
|
||||
})
|
||||
|
||||
test_that("runTests works with a dir app that calls modules that return reactives and use brushing", {
|
||||
skip_if_not_installed("ggplot2")
|
||||
app <- test_path("..", "test-modules", "107_scatterplot")
|
||||
run <- testthat::expect_output(
|
||||
print(runTests(app)),
|
||||
|
||||
@@ -288,6 +288,9 @@ test_that("works with async", {
|
||||
})
|
||||
|
||||
test_that("works with multiple promises in parallel", {
|
||||
# This test is inherently about timing which is against CRAN's policy.
|
||||
testthat::skip_on_cran()
|
||||
|
||||
server <- function(input, output, session) {
|
||||
output$txt1 <- renderText({
|
||||
future({
|
||||
|
||||
10
tsconfig.eslint.json
Normal file
10
tsconfig.eslint.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"srcts/types/"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user