mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-11 16:08:19 -05:00
Compare commits
6 Commits
feat/input
...
fix/bindin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42b6b8bdde | ||
|
|
4a01dde46b | ||
|
|
6c281f377c | ||
|
|
33d6686223 | ||
|
|
20b2669e76 | ||
|
|
db123a1508 |
@@ -26,6 +26,7 @@
|
||||
^\.vscode$
|
||||
^\.madgerc$
|
||||
^\.prettierrc\.yml$
|
||||
^babel\.config\.json$
|
||||
^jest\.config\.js$
|
||||
^package\.json$
|
||||
^tsconfig\.json$
|
||||
@@ -36,4 +37,3 @@
|
||||
^\.browserslistrc$
|
||||
^\.eslintrc\.yml$
|
||||
^\.yarnrc\.yml$
|
||||
^_dev$
|
||||
|
||||
10
DESCRIPTION
10
DESCRIPTION
@@ -1,7 +1,7 @@
|
||||
Package: shiny
|
||||
Type: Package
|
||||
Title: Web Application Framework for R
|
||||
Version: 1.10.0.9001
|
||||
Version: 1.10.0.9000
|
||||
Authors@R: c(
|
||||
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@posit.co", comment = c(ORCID = "0000-0002-1576-2126")),
|
||||
person("Joe", "Cheng", role = "aut", email = "joe@posit.co"),
|
||||
@@ -85,7 +85,7 @@ Imports:
|
||||
later (>= 1.0.0),
|
||||
promises (>= 1.3.2),
|
||||
tools,
|
||||
cli,
|
||||
crayon,
|
||||
rlang (>= 0.4.10),
|
||||
fastmap (>= 1.1.1),
|
||||
withr,
|
||||
@@ -99,7 +99,7 @@ Suggests:
|
||||
datasets,
|
||||
DT,
|
||||
Cairo (>= 1.5-5),
|
||||
testthat (>= 3.2.1),
|
||||
testthat (>= 3.0.0),
|
||||
knitr (>= 1.6),
|
||||
markdown,
|
||||
rmarkdown,
|
||||
@@ -111,8 +111,7 @@ Suggests:
|
||||
dygraphs,
|
||||
ragg,
|
||||
showtext,
|
||||
sass,
|
||||
watcher
|
||||
sass
|
||||
URL: https://shiny.posit.co/,
|
||||
https://github.com/rstudio/shiny
|
||||
BugReports: https://github.com/rstudio/shiny/issues
|
||||
@@ -211,6 +210,7 @@ Collate:
|
||||
RoxygenNote: 7.3.2
|
||||
Encoding: UTF-8
|
||||
Roxygen: list(markdown = TRUE)
|
||||
RdMacros: lifecycle
|
||||
Config/testthat/edition: 3
|
||||
Config/Needs/check:
|
||||
shinytest2
|
||||
|
||||
29
NEWS.md
29
NEWS.md
@@ -1,38 +1,9 @@
|
||||
# shiny (development version)
|
||||
|
||||
## New features
|
||||
|
||||
* `textInput()`, `textAreaInput()`, `numericInput()` and `passwordInput()` all gain an `updateOn` option. `updateOn = "change"` is the default and previous behavior, where the input value updates immediately whenever the value changes. With `updateOn = "blur"`, the input value will update only when the text input loses focus or when the user presses Enter (or Cmd/Ctrl + Enter for `textAreaInput()`). (#4183)
|
||||
|
||||
* `textAreaInput()` gains a `autoresize` option, which automatically resizes the text area to fit its content.
|
||||
|
||||
* The `callback` argument of Shiny.js' `InputBinding.subscribe()` method gains support for a value of `"event"`. This makes it possible for an input binding to use event priority when updating the value (i.e., send immediately and always resend, even if the value hasn't changed).
|
||||
|
||||
## Changes
|
||||
|
||||
* Shiny no longer suspends input changes when _any_ `<input type="submit">` or `<button type="submit">` is on the page. Instead, it now only suspends when a `submitButton()` is present. If you have reason for creating a submit button from custom HTML, add a CSS class of `shiny-submit-button` to the button.
|
||||
|
||||
## Improvements
|
||||
|
||||
* When auto-reload is enabled, Shiny now reloads the entire app when support files, like Shiny modules, additional script files, or web assets, change. To enable auto-reload, call `devmode(TRUE)` to enable Shiny's developer mode, or set `options(shiny.autoreload = TRUE)` to specifically enable auto-reload. You can choose which files are watched for changes with the `shiny.autoreload.pattern` option. (#4184)
|
||||
|
||||
* When busy indicators are enabled (i.e., `useBusyIndicators()`), Shiny now:
|
||||
* Shows a spinner on recalculating htmlwidgets that have previously rendered an error (including `req()` and `validate()`). (#4172)
|
||||
* Shows a spinner on `tableOutput()`. (#4172)
|
||||
* Places a minimum height on recalculating outputs so that the spinner is always visible. (#4172)
|
||||
|
||||
* Shiny now uses `{cli}` instead of `{crayon}` for rich log messages. (@olivroy #4170)
|
||||
|
||||
* Shiny's Typescript assets are now compiled to ES2021 instead of ES5. (#4066)
|
||||
|
||||
## Bug fixes
|
||||
|
||||
* Fixed a bug with modals where calling `removeModal()` too quickly after `showModal()` would fail to remove the modal if the remove modal message was received while the modal was in the process of being revealed. (#4173)
|
||||
|
||||
* The Shiny Client Console (enabled with `shiny::devmode()`) no longer displays duplicate warning or error message. (#4177)
|
||||
|
||||
* Updated the JavaScript used when inserting a tab to avoid rendering dynamic UI elements twice when adding the new tab via `insertTab()` or `bslib::nav_insert()`. (#4179)
|
||||
|
||||
# shiny 1.10.0
|
||||
|
||||
## New features and improvements
|
||||
|
||||
@@ -1113,7 +1113,7 @@ plotOutput <- function(outputId, width = "100%", height="400px",
|
||||
#' @rdname renderTable
|
||||
#' @export
|
||||
tableOutput <- function(outputId) {
|
||||
div(id = outputId, class="shiny-html-output shiny-table-output")
|
||||
div(id = outputId, class="shiny-html-output")
|
||||
}
|
||||
|
||||
dataTableDependency <- list(
|
||||
|
||||
@@ -417,11 +417,11 @@ printOneStackTrace <- function(stackTrace, stripResult, full, offset) {
|
||||
": ",
|
||||
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
|
||||
if (category == "pkg")
|
||||
cli::col_silver(name)
|
||||
crayon::silver(name)
|
||||
else if (category == "user")
|
||||
cli::style_bold(cli::col_blue(name))
|
||||
crayon::blue$bold(name)
|
||||
else
|
||||
cli::col_white(name)
|
||||
crayon::white(name)
|
||||
}),
|
||||
"\n"
|
||||
)
|
||||
|
||||
@@ -29,36 +29,22 @@
|
||||
#' A numeric vector of length 1.
|
||||
#'
|
||||
#' @export
|
||||
numericInput <- function(
|
||||
inputId,
|
||||
label,
|
||||
value,
|
||||
min = NA,
|
||||
max = NA,
|
||||
step = NA,
|
||||
width = NULL,
|
||||
...,
|
||||
updateOn = c("change", "blur")
|
||||
) {
|
||||
rlang::check_dots_empty()
|
||||
updateOn <- rlang::arg_match(updateOn)
|
||||
numericInput <- function(inputId, label, value, min = NA, max = NA, step = NA,
|
||||
width = NULL) {
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
# build input tag
|
||||
inputTag <- tags$input(
|
||||
id = inputId,
|
||||
type = "number",
|
||||
class = "shiny-input-number form-control",
|
||||
value = formatNoSci(value),
|
||||
`data-update-on` = updateOn
|
||||
)
|
||||
if (!is.na(min)) inputTag$attribs$min = min
|
||||
if (!is.na(max)) inputTag$attribs$max = max
|
||||
if (!is.na(step)) inputTag$attribs$step = step
|
||||
inputTag <- tags$input(id = inputId, type = "number", class="shiny-input-number form-control",
|
||||
value = formatNoSci(value))
|
||||
if (!is.na(min))
|
||||
inputTag$attribs$min = min
|
||||
if (!is.na(max))
|
||||
inputTag$attribs$max = max
|
||||
if (!is.na(step))
|
||||
inputTag$attribs$step = step
|
||||
|
||||
div(
|
||||
class = "form-group shiny-input-container",
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
inputTag
|
||||
|
||||
@@ -30,29 +30,12 @@
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
passwordInput <- function(
|
||||
inputId,
|
||||
label,
|
||||
value = "",
|
||||
width = NULL,
|
||||
placeholder = NULL,
|
||||
...,
|
||||
updateOn = c("change", "blur")
|
||||
) {
|
||||
rlang::check_dots_empty()
|
||||
updateOn <- rlang::arg_match(updateOn)
|
||||
|
||||
div(
|
||||
class = "form-group shiny-input-container",
|
||||
passwordInput <- function(inputId, label, value = "", width = NULL,
|
||||
placeholder = NULL) {
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$input(
|
||||
id = inputId,
|
||||
type = "password",
|
||||
class = "shiny-input-password form-control",
|
||||
value = value,
|
||||
placeholder = placeholder,
|
||||
`data-update-on` = updateOn
|
||||
)
|
||||
tags$input(id = inputId, type="password", class="shiny-input-password form-control", value=value,
|
||||
placeholder = placeholder)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ submitButton <- function(text = "Apply Changes", icon = NULL, width = NULL) {
|
||||
div(
|
||||
tags$button(
|
||||
type="submit",
|
||||
class="btn btn-primary submit-button",
|
||||
class="btn btn-primary",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
list(icon, text)
|
||||
)
|
||||
|
||||
@@ -10,14 +10,6 @@
|
||||
#' @param placeholder A character string giving the user a hint as to what can
|
||||
#' be entered into the control. Internet Explorer 8 and 9 do not support this
|
||||
#' option.
|
||||
#' @param ... Ignored, included to require named arguments and for future
|
||||
#' feature expansion.
|
||||
#' @param updateOn A character vector specifying when the input should be
|
||||
#' updated. Options are `"change"` (default) and `"blur"`. Use `"change"` to
|
||||
#' update the input immediately whenever the value changes. Use `"blur"`to
|
||||
#' delay the input update until the input loses focus (the user moves away
|
||||
#' from the input), or when Enter is pressed (or Cmd/Ctrl + Enter for
|
||||
#' [textAreaInput()]).
|
||||
#' @return A text input control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
@@ -42,31 +34,15 @@
|
||||
#' unless `value` is provided.
|
||||
#'
|
||||
#' @export
|
||||
textInput <- function(
|
||||
inputId,
|
||||
label,
|
||||
value = "",
|
||||
width = NULL,
|
||||
placeholder = NULL,
|
||||
...,
|
||||
updateOn = c("change", "blur")
|
||||
) {
|
||||
rlang::check_dots_empty()
|
||||
updateOn <- rlang::arg_match(updateOn)
|
||||
textInput <- function(inputId, label, value = "", width = NULL,
|
||||
placeholder = NULL) {
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
div(
|
||||
class = "form-group shiny-input-container",
|
||||
div(class = "form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
tags$input(
|
||||
id = inputId,
|
||||
type = "text",
|
||||
class = "shiny-input-text form-control",
|
||||
value = value,
|
||||
placeholder = placeholder,
|
||||
`data-update-on` = updateOn
|
||||
)
|
||||
tags$input(id = inputId, type="text", class="shiny-input-text form-control", value=value,
|
||||
placeholder = placeholder)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
#' @param resize Which directions the textarea box can be resized. Can be one of
|
||||
#' `"both"`, `"none"`, `"vertical"`, and `"horizontal"`. The default, `NULL`,
|
||||
#' will use the client browser's default setting for resizing textareas.
|
||||
#' @param autoresize If `TRUE`, the textarea will automatically resize to fit
|
||||
#' the input text.
|
||||
#' @return A textarea input control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
@@ -43,22 +41,8 @@
|
||||
#' unless `value` is provided.
|
||||
#'
|
||||
#' @export
|
||||
textAreaInput <- function(
|
||||
inputId,
|
||||
label,
|
||||
value = "",
|
||||
width = NULL,
|
||||
height = NULL,
|
||||
cols = NULL,
|
||||
rows = NULL,
|
||||
placeholder = NULL,
|
||||
resize = NULL,
|
||||
...,
|
||||
autoresize = FALSE,
|
||||
updateOn = c("change", "blur")
|
||||
) {
|
||||
rlang::check_dots_empty()
|
||||
updateOn <- rlang::arg_match(updateOn)
|
||||
textAreaInput <- function(inputId, label, value = "", width = NULL, height = NULL,
|
||||
cols = NULL, rows = NULL, placeholder = NULL, resize = NULL) {
|
||||
|
||||
value <- restoreInput(id = inputId, default = value)
|
||||
|
||||
@@ -66,30 +50,23 @@ textAreaInput <- function(
|
||||
resize <- match.arg(resize, c("both", "none", "vertical", "horizontal"))
|
||||
}
|
||||
|
||||
classes <- c("shiny-input-textarea", "form-control")
|
||||
if (autoresize) {
|
||||
classes <- c(classes, "textarea-autoresize")
|
||||
if (is.null(rows)) {
|
||||
rows <- 1
|
||||
}
|
||||
}
|
||||
style <- css(
|
||||
# The width is specified on the parent div.
|
||||
width = if (!is.null(width)) "100%",
|
||||
height = validateCssUnit(height),
|
||||
resize = resize
|
||||
)
|
||||
|
||||
div(
|
||||
class = "form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
div(class = "form-group shiny-input-container",
|
||||
shinyInputLabel(inputId, label),
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
tags$textarea(
|
||||
id = inputId,
|
||||
class = classes,
|
||||
class = "shiny-input-textarea form-control",
|
||||
placeholder = placeholder,
|
||||
style = css(
|
||||
width = if (!is.null(width)) "100%",
|
||||
height = validateCssUnit(height),
|
||||
resize = resize
|
||||
),
|
||||
style = style,
|
||||
rows = rows,
|
||||
cols = cols,
|
||||
`data-update-on` = updateOn,
|
||||
value
|
||||
)
|
||||
)
|
||||
|
||||
@@ -65,20 +65,16 @@ getShinyOption <- function(name, default = NULL) {
|
||||
#' changes are detected, all connected Shiny sessions are reloaded. This
|
||||
#' allows for fast feedback loops when tweaking Shiny UI.
|
||||
#'
|
||||
#' Monitoring for changes is no longer expensive, thanks to the \pkg{watcher}
|
||||
#' package, but this feature is still intended only for development.
|
||||
#' Since monitoring for changes is expensive (we simply poll for last
|
||||
#' modified times), this feature is intended only for development.
|
||||
#'
|
||||
#' You can customize the file patterns Shiny will monitor by setting the
|
||||
#' shiny.autoreload.pattern option. For example, to monitor only `ui.R`:
|
||||
#' `options(shiny.autoreload.pattern = glob2rx("ui.R"))`.
|
||||
#' shiny.autoreload.pattern option. For example, to monitor only ui.R:
|
||||
#' `options(shiny.autoreload.pattern = glob2rx("ui.R"))`
|
||||
#'
|
||||
#' As mentioned above, Shiny no longer polls watched files for changes.
|
||||
#' Instead, using \pkg{watcher}, Shiny is notified of file changes as they
|
||||
#' occur. These changes are batched together within a customizable latency
|
||||
#' period. You can adjust this period by setting
|
||||
#' `options(shiny.autoreload.interval = 2000)` (in milliseconds). This value
|
||||
#' converted to seconds and passed to the `latency` argument of
|
||||
#' [watcher::watcher()]. The default latency is 250ms.}
|
||||
#' The default polling interval is 500 milliseconds. You can change this
|
||||
#' by setting e.g. `options(shiny.autoreload.interval = 2000)` (every
|
||||
#' two seconds).}
|
||||
#' \item{shiny.deprecation.messages (defaults to `TRUE`)}{This controls whether messages for
|
||||
#' deprecated functions in Shiny will be printed. See
|
||||
#' [shinyDeprecated()] for more information.}
|
||||
|
||||
114
R/shinyapp.R
114
R/shinyapp.R
@@ -162,29 +162,11 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
|
||||
sharedEnv <- globalenv()
|
||||
}
|
||||
|
||||
# To enable hot-reloading of support files, this function is called
|
||||
# whenever the UI or Server func source is updated. To avoid loading
|
||||
# support files 2x, we follow the last cache update trigger timestamp.
|
||||
autoload_r_support_if_needed <- local({
|
||||
autoload_last_loaded <- -1
|
||||
function() {
|
||||
if (!isTRUE(getOption("shiny.autoload.r", TRUE))) return()
|
||||
|
||||
last_cache_trigger <- cachedAutoReloadLastChanged$get()
|
||||
if (identical(autoload_last_loaded, last_cache_trigger)) return()
|
||||
|
||||
loadSupport(appDir, renv = sharedEnv, globalrenv = globalenv())
|
||||
|
||||
autoload_last_loaded <<- last_cache_trigger
|
||||
}
|
||||
})
|
||||
|
||||
# uiHandlerSource is a function that returns an HTTP handler for serving up
|
||||
# ui.R as a webpage. The "cachedFuncWithFile" call makes sure that the closure
|
||||
# we're creating here only gets executed when ui.R's contents change.
|
||||
uiHandlerSource <- cachedFuncWithFile(appDir, "ui.R", case.sensitive = FALSE,
|
||||
function(uiR) {
|
||||
autoload_r_support_if_needed()
|
||||
if (file.exists(uiR)) {
|
||||
# If ui.R contains a call to shinyUI (which sets .globals$ui), use that.
|
||||
# If not, then take the last expression that's returned from ui.R.
|
||||
@@ -215,7 +197,6 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
|
||||
|
||||
serverSource <- cachedFuncWithFile(appDir, "server.R", case.sensitive = FALSE,
|
||||
function(serverR) {
|
||||
autoload_r_support_if_needed()
|
||||
# If server.R contains a call to shinyServer (which sets .globals$server),
|
||||
# use that. If not, then take the last expression that's returned from
|
||||
# server.R.
|
||||
@@ -251,9 +232,10 @@ shinyAppDir_serverR <- function(appDir, options=list()) {
|
||||
onStart <- function() {
|
||||
oldwd <<- getwd()
|
||||
setwd(appDir)
|
||||
# TODO: we should support hot reloading on global.R and R/*.R changes.
|
||||
if (getOption("shiny.autoload.r", TRUE)) {
|
||||
autoload_r_support_if_needed()
|
||||
} else {
|
||||
loadSupport(appDir, renv=sharedEnv, globalrenv=globalenv())
|
||||
} else {
|
||||
if (file.exists(file.path.ci(appDir, "global.R")))
|
||||
sourceUTF8(file.path.ci(appDir, "global.R"))
|
||||
}
|
||||
@@ -308,77 +290,33 @@ initAutoReloadMonitor <- function(dir) {
|
||||
return(function(){})
|
||||
}
|
||||
|
||||
filePattern <- getOption(
|
||||
"shiny.autoreload.pattern",
|
||||
".*\\.(r|html?|js|css|png|jpe?g|gif)$"
|
||||
)
|
||||
filePattern <- getOption("shiny.autoreload.pattern",
|
||||
".*\\.(r|html?|js|css|png|jpe?g|gif)$")
|
||||
|
||||
|
||||
if (is_installed("watcher")) {
|
||||
check_for_update <- function(paths) {
|
||||
paths <- grep(
|
||||
filePattern,
|
||||
paths,
|
||||
ignore.case = TRUE,
|
||||
value = TRUE
|
||||
)
|
||||
|
||||
if (length(paths) == 0) {
|
||||
return()
|
||||
}
|
||||
|
||||
cachedAutoReloadLastChanged$set()
|
||||
lastValue <- NULL
|
||||
observeLabel <- paste0("File Auto-Reload - '", basename(dir), "'")
|
||||
obs <- observe(label = observeLabel, {
|
||||
files <- sort_c(
|
||||
list.files(dir, pattern = filePattern, recursive = TRUE, ignore.case = TRUE)
|
||||
)
|
||||
times <- file.info(files)$mtime
|
||||
names(times) <- files
|
||||
|
||||
if (is.null(lastValue)) {
|
||||
# First run
|
||||
lastValue <<- times
|
||||
} else if (!identical(lastValue, times)) {
|
||||
# We've changed!
|
||||
lastValue <<- times
|
||||
autoReloadCallbacks$invoke()
|
||||
}
|
||||
|
||||
# [garrick, 2025-02-20] Shiny <= v1.10.0 used `invalidateLater()` with an
|
||||
# autoreload.interval in ms. {watcher} instead uses a latency parameter in
|
||||
# seconds, which serves a similar purpose and that I'm keeping for backcompat.
|
||||
latency <- getOption("shiny.autoreload.interval", 250) / 1000
|
||||
watcher <- watcher::watcher(dir, check_for_update, latency = latency)
|
||||
watcher$start()
|
||||
onStop(watcher$stop)
|
||||
} else {
|
||||
# Fall back to legacy observer behavior
|
||||
if (!is_false(getOption("shiny.autoreload.legacy_warning", TRUE))) {
|
||||
cli::cli_warn(
|
||||
c(
|
||||
"Using legacy autoreload file watching. Please install {.pkg watcher} for a more performant autoreload file watcher.",
|
||||
"i" = "Set {.run options(shiny.autoreload.legacy_warning = FALSE)} to suppress this warning."
|
||||
),
|
||||
.frequency = "regularly",
|
||||
.frequency_id = "shiny.autoreload.legacy_warning"
|
||||
)
|
||||
}
|
||||
|
||||
lastValue <- NULL
|
||||
observeLabel <- paste0("File Auto-Reload - '", basename(dir), "'")
|
||||
watcher <- observe(label = observeLabel, {
|
||||
files <- sort_c(
|
||||
list.files(dir, pattern = filePattern, recursive = TRUE, ignore.case = TRUE)
|
||||
)
|
||||
times <- file.info(files)$mtime
|
||||
names(times) <- files
|
||||
|
||||
if (is.null(lastValue)) {
|
||||
# First run
|
||||
lastValue <<- times
|
||||
} else if (!identical(lastValue, times)) {
|
||||
# We've changed!
|
||||
lastValue <<- times
|
||||
cachedAutoReloadLastChanged$set()
|
||||
autoReloadCallbacks$invoke()
|
||||
}
|
||||
|
||||
invalidateLater(getOption("shiny.autoreload.interval", 500))
|
||||
})
|
||||
|
||||
onStop(watcher$destroy)
|
||||
|
||||
watcher$destroy
|
||||
}
|
||||
invalidateLater(getOption("shiny.autoreload.interval", 500))
|
||||
})
|
||||
|
||||
invisible(watcher)
|
||||
onStop(obs$destroy)
|
||||
|
||||
obs$destroy
|
||||
}
|
||||
|
||||
#' Load an app's supporting R files
|
||||
@@ -483,6 +421,8 @@ shinyAppDir_appR <- function(fileName, appDir, options=list())
|
||||
wasDir <- setwd(appDir)
|
||||
on.exit(setwd(wasDir))
|
||||
|
||||
# TODO: we should support hot reloading on R/*.R changes.
|
||||
# In an upcoming version of shiny, this option will go away.
|
||||
if (getOption("shiny.autoload.r", TRUE)) {
|
||||
# Create a child env which contains all the helpers and will be the shared parent
|
||||
# of the ui.R and server.R load.
|
||||
|
||||
@@ -383,10 +383,8 @@ markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
|
||||
#' The corresponding HTML output tag should be `div` or `img` and have
|
||||
#' the CSS class name `shiny-image-output`.
|
||||
#'
|
||||
#' @seealso
|
||||
#' * For more details on how the images are generated, and how to control
|
||||
#' @seealso For more details on how the images are generated, and how to control
|
||||
#' the output, see [plotPNG()].
|
||||
#' * Use [outputOptions()] to set general output options for an image output.
|
||||
#'
|
||||
#' @param expr An expression that returns a list.
|
||||
#' @inheritParams renderUI
|
||||
@@ -600,7 +598,6 @@ isTemp <- function(path, tempDir = tempdir(), mustExist) {
|
||||
#' used in an interactive RMarkdown document.
|
||||
#'
|
||||
#' @example res/text-example.R
|
||||
#' @seealso [outputOptions()]
|
||||
#' @export
|
||||
renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
width = getOption('width'), outputArgs=list())
|
||||
@@ -722,7 +719,7 @@ renderText <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
#' call to [uiOutput()] when `renderUI` is used in an
|
||||
#' interactive R Markdown document.
|
||||
#'
|
||||
#' @seealso [uiOutput()], [outputOptions()]
|
||||
#' @seealso [uiOutput()]
|
||||
#' @export
|
||||
#' @examples
|
||||
#' ## Only run examples in interactive R sessions
|
||||
@@ -812,13 +809,6 @@ renderUI <- function(expr, env = parent.frame(), quoted = FALSE,
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' @seealso
|
||||
#' * The download handler, like other outputs, is suspended (disabled) by
|
||||
#' default for download buttons and links that are hidden. Use
|
||||
#' [outputOptions()] to control this behavior, e.g. to set
|
||||
#' `suspendWhenHidden = FALSE` if the download is initiated by
|
||||
#' programmatically clicking on the download button using JavaScript.
|
||||
#' @export
|
||||
downloadHandler <- function(filename, content, contentType=NULL, outputArgs=list()) {
|
||||
renderFunc <- function(shinysession, name, ...) {
|
||||
|
||||
8
R/test.R
8
R/test.R
@@ -158,7 +158,8 @@ print.shiny_runtests <- function(x, ..., reporter = "summary") {
|
||||
|
||||
|
||||
if (any(x$pass)) {
|
||||
cli::cat_bullet("Success", bullet = "tick", bullet_col = "green")
|
||||
# TODO in future... use clisymbols::symbol$tick and crayon green
|
||||
cat("* Success\n")
|
||||
mapply(
|
||||
x$file,
|
||||
x$pass,
|
||||
@@ -170,8 +171,9 @@ print.shiny_runtests <- function(x, ..., reporter = "summary") {
|
||||
}
|
||||
)
|
||||
}
|
||||
if (!all(x$pass)) {
|
||||
cli::cat_bullet("Failure", bullet = "cross", bullet_col = "red")
|
||||
if (any(!x$pass)) {
|
||||
# TODO in future... use clisymbols::symbol$cross and crayon red
|
||||
cat("* Failure\n")
|
||||
mapply(
|
||||
x$file,
|
||||
x$pass,
|
||||
|
||||
37
R/utils.R
37
R/utils.R
@@ -770,45 +770,22 @@ formatNoSci <- function(x) {
|
||||
format(x, scientific = FALSE, digits = 15)
|
||||
}
|
||||
|
||||
# A simple getter/setting to track the last time the auto-reload process
|
||||
# updated. This value is used by `cachedFuncWithFile()` when auto-reload is
|
||||
# enabled to reload app/ui/server files when watched supporting files change.
|
||||
cachedAutoReloadLastChanged <- local({
|
||||
last_update <- 0
|
||||
|
||||
list(
|
||||
set = function() {
|
||||
last_update <<- as.integer(Sys.time())
|
||||
invisible(last_update)
|
||||
},
|
||||
get = function() {
|
||||
last_update
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
# Returns a function that calls the given func and caches the result for
|
||||
# subsequent calls, unless the given file's mtime changes.
|
||||
cachedFuncWithFile <- function(dir, file, func, case.sensitive = FALSE) {
|
||||
dir <- normalizePath(dir, mustWork = TRUE)
|
||||
|
||||
dir <- normalizePath(dir, mustWork=TRUE)
|
||||
mtime <- NA
|
||||
value <- NULL
|
||||
last_mtime_file <- NA
|
||||
last_autoreload <- 0
|
||||
|
||||
function(...) {
|
||||
fname <- if (case.sensitive) {
|
||||
file.path(dir, file)
|
||||
} else {
|
||||
fname <- if (case.sensitive)
|
||||
file.path(dir, file)
|
||||
else
|
||||
file.path.ci(dir, file)
|
||||
}
|
||||
|
||||
now <- file.info(fname)$mtime
|
||||
autoreload <- last_autoreload < cachedAutoReloadLastChanged$get()
|
||||
if (autoreload || !identical(last_mtime_file, now)) {
|
||||
if (!identical(mtime, now)) {
|
||||
value <<- func(fname, ...)
|
||||
last_mtime_file <<- now
|
||||
last_autoreload <<- cachedAutoReloadLastChanged$get()
|
||||
mtime <<- now
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
15
babel.config.json
Normal file
15
babel.config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-typescript",
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"useBuiltIns": "usage",
|
||||
"corejs": "3.12"
|
||||
}
|
||||
]
|
||||
],
|
||||
"ignore":[
|
||||
"node_modules/core-js"
|
||||
]
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
/*! shiny 1.10.0.9000 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | 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}
|
||||
:where([data-shiny-busy-spinners] .recalculating){position:relative}[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.shiny-html-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(#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}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
Selectize.define("selectize-plugin-a11y",function(c){var t=this,l=13;typeof t.accessibility>"u"&&(t.accessibility={}),t.accessibility.helpers={randomId:function(e){for(var r="",s=e||10,i="abcdefghijklmnopqrstuvwxyz0123456789",n=i.length,a=0;a<s;a++)r+=i[Math.floor(n*Math.random())];return r}},t.accessibility.liveRegion={$region:"",speak:function(e){var r=$("<div></div>");r.text(e),this.$region.html(r)},domListener:function(){var e=new MutationObserver(function(r){r.forEach(function(s){var i=$(s.target);if(i.hasClass("items"))if(i.hasClass("dropdown-active")){t.$control_input.attr("aria-expanded","true");for(var n=t.$dropdown_content[0].children,a=0;a<n.length;a++){var o=n[a].attributes;o.role||n[a].setAttribute("role","option"),o.id||n[a].setAttribute("id",t.accessibility.helpers.randomId())}}else t.$control_input.attr("aria-expanded","false"),t.$control_input.removeAttr("aria-activedescendant");else i.hasClass("active")&&i.attr("data-value")&&(t.$control_input.attr("aria-activedescendant",i.attr("id")),t.accessibility.liveRegion.speak(i.text(),500))})});e.observe(t.$dropdown[0],{attributes:!0,attributeFilter:["class"],subtree:!0,attributeOldValue:!0}),e.observe(t.$control[0],{attributes:!0,attributeFilter:["class"]}),e.observe(t.$control_input[0],{attributes:!0,attributeFilter:["value"]})},setAttributes:function(){this.$region.attr({"aria-live":"assertive",role:"log","aria-relevant":"additions","aria-atomic":"true"})},setStyles:function(){this.$region.css({position:"absolute",width:"1px",height:"1px","margin-top":"-1px",clip:"rect(1px, 1px, 1px, 1px)",overflow:"hidden"})},init:function(){this.$region=$("<div>"),this.setAttributes(),this.setStyles(),$("body").append(this.$region),this.domListener()}},this.setup=function(){var e=t.setup;return function(){e.apply(this,arguments);var r=t.accessibility.helpers.randomId(),s=t.accessibility.helpers.randomId();t.$control.on("keydown",function(i){i.keyCode===l&&(t.settings.openOnFocus?(t.settings.openOnFocus=!1,t.focus(),setTimeout(function(){t.settings.openOnFocus=!0},0)):t.focus())}),t.$control_input.attr({role:"combobox","aria-expanded":"false",haspopup:"listbox","aria-owns":s,"aria-label":t.$wrapper.closest("[data-accessibility-selectize-label]").attr("data-accessibility-selectize-label")}),t.$dropdown_content.attr({role:"listbox",id:s}),t.accessibility.liveRegion.init()}}(),this.destroy=function(){var e=t.destroy;return function(){return t.accessibility.liveRegion.$region.remove(),e.apply(this,arguments)}}()});
|
||||
Selectize.define("selectize-plugin-a11y",function(c){var t=this,l=13;typeof t.accessibility=="undefined"&&(t.accessibility={}),t.accessibility.helpers={randomId:function(e){for(var r="",s=e||10,i="abcdefghijklmnopqrstuvwxyz0123456789",n=i.length,a=0;a<s;a++)r+=i[Math.floor(n*Math.random())];return r}},t.accessibility.liveRegion={$region:"",speak:function(e){var r=$("<div></div>");r.text(e),this.$region.html(r)},domListener:function(){var e=new MutationObserver(function(r){r.forEach(function(s){var i=$(s.target);if(i.hasClass("items"))if(i.hasClass("dropdown-active")){t.$control_input.attr("aria-expanded","true");for(var n=t.$dropdown_content[0].children,a=0;a<n.length;a++){var o=n[a].attributes;o.role||n[a].setAttribute("role","option"),o.id||n[a].setAttribute("id",t.accessibility.helpers.randomId())}}else t.$control_input.attr("aria-expanded","false"),t.$control_input.removeAttr("aria-activedescendant");else i.hasClass("active")&&i.attr("data-value")&&(t.$control_input.attr("aria-activedescendant",i.attr("id")),t.accessibility.liveRegion.speak(i.text(),500))})});e.observe(t.$dropdown[0],{attributes:!0,attributeFilter:["class"],subtree:!0,attributeOldValue:!0}),e.observe(t.$control[0],{attributes:!0,attributeFilter:["class"]}),e.observe(t.$control_input[0],{attributes:!0,attributeFilter:["value"]})},setAttributes:function(){this.$region.attr({"aria-live":"assertive",role:"log","aria-relevant":"additions","aria-atomic":"true"})},setStyles:function(){this.$region.css({position:"absolute",width:"1px",height:"1px","margin-top":"-1px",clip:"rect(1px, 1px, 1px, 1px)",overflow:"hidden"})},init:function(){this.$region=$("<div>"),this.setAttributes(),this.setStyles(),$("body").append(this.$region),this.domListener()}},this.setup=function(){var e=t.setup;return function(){e.apply(this,arguments);var r=t.accessibility.helpers.randomId(),s=t.accessibility.helpers.randomId();t.$control.on("keydown",function(i){i.keyCode===l&&(t.settings.openOnFocus?(t.settings.openOnFocus=!1,t.focus(),setTimeout(function(){t.settings.openOnFocus=!0},0)):t.focus())}),t.$control_input.attr({role:"combobox","aria-expanded":"false",haspopup:"listbox","aria-owns":s,"aria-label":t.$wrapper.closest("[data-accessibility-selectize-label]").attr("data-accessibility-selectize-label")}),t.$dropdown_content.attr({role:"listbox",id:s}),t.accessibility.liveRegion.init()}}(),this.destroy=function(){var e=t.destroy;return function(){return t.accessibility.liveRegion.$region.remove(),e.apply(this,arguments)}}()});
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
||||
/*! shiny 1.10.0.9000 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
|
||||
"use strict";(()=>{var t=eval;window.addEventListener("message",function(a){let e=a.data;e.code&&t(e.code)});})();
|
||||
"use strict";(function(){var a=eval;window.addEventListener("message",function(i){var e=i.data;e.code&&a(e.code)});})();
|
||||
//# sourceMappingURL=shiny-testmode.js.map
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../../srcts/src/utils/eval.ts", "../../../srcts/extras/shiny-testmode.ts"],
|
||||
"sourcesContent": ["//esbuild.github.io/content-types/#direct-eval\n//tl/dr;\n// * Direct usage of `eval(\"x\")` is bad with bundled code.\n// * Instead, use indirect calls to `eval` such as `indirectEval(\"x\")`\n// * Even just renaming the function works well enough.\n// > This is known as \"indirect eval\" because eval is not being called directly, and so does not trigger the grammatical special case for direct eval in the JavaScript VM. You can call indirect eval using any syntax at all except for an expression of the exact form eval('x'). For example, var eval2 = eval; eval2('x') and [eval][0]('x') and window.eval('x') are all indirect eval calls.\n// > When you use indirect eval, the code is evaluated in the global scope instead of in the inline scope of the caller.\n\nconst indirectEval = eval;\n\nexport { indirectEval };\n", "/* eslint-disable unicorn/filename-case */\nimport { indirectEval } from \"../src/utils/eval\";\n\n// Listen for messages from parent frame. This file is only added when the\n// shiny.testmode option is TRUE.\nwindow.addEventListener(\"message\", function (e: { data: { code: string } }) {\n const message = e.data;\n\n if (message.code) indirectEval(message.code);\n});\n"],
|
||||
"mappings": ";mBAQA,IAAMA,EAAe,KCHrB,OAAO,iBAAiB,UAAW,SAAUC,EAA+B,CAC1E,IAAMC,EAAUD,EAAE,KAEdC,EAAQ,MAAMC,EAAaD,EAAQ,IAAI,CAC7C,CAAC",
|
||||
"sourcesContent": ["//esbuild.github.io/content-types/#direct-eval\n//tl/dr;\n// * Direct usage of `eval(\"x\")` is bad with bundled code.\n// * Instead, use indirect calls to `eval` such as `indirectEval(\"x\")`\n// * Even just renaming the function works well enough.\n// > This is known as \"indirect eval\" because eval is not being called directly, and so does not trigger the grammatical special case for direct eval in the JavaScript VM. You can call indirect eval using any syntax at all except for an expression of the exact form eval('x'). For example, var eval2 = eval; eval2('x') and [eval][0]('x') and window.eval('x') are all indirect eval calls.\n// > When you use indirect eval, the code is evaluated in the global scope instead of in the inline scope of the caller.\n\nvar indirectEval = eval;\nexport { indirectEval };", "/* eslint-disable unicorn/filename-case */\nimport { indirectEval } from \"../src/utils/eval\";\n\n// Listen for messages from parent frame. This file is only added when the\n// shiny.testmode option is TRUE.\nwindow.addEventListener(\"message\", function (e) {\n var message = e.data;\n if (message.code) indirectEval(message.code);\n});"],
|
||||
"mappings": ";yBAQA,IAAIA,EAAe,KCHnB,OAAO,iBAAiB,UAAW,SAAUC,EAAG,CAC9C,IAAIC,EAAUD,EAAE,KACZC,EAAQ,MAAMC,EAAaD,EAAQ,IAAI,CAC7C,CAAC",
|
||||
"names": ["indirectEval", "e", "message", "indirectEval"]
|
||||
}
|
||||
|
||||
28000
inst/www/shared/shiny.js
28000
inst/www/shared/shiny.js
File diff suppressed because it is too large
Load Diff
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
447
inst/www/shared/shiny.min.js
vendored
447
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
@@ -384,14 +384,6 @@ html.autoreload-enabled #shiny-disconnected-overlay.reloading {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Styling for inputTextArea(autoresize=TRUE) */
|
||||
.textarea-autoresize textarea.form-control {
|
||||
padding: 5px 8px;
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
#shiny-notification-panel {
|
||||
position: fixed;
|
||||
|
||||
@@ -60,14 +60,4 @@ server <- function(input, output) {
|
||||
|
||||
shinyApp(ui, server)
|
||||
}
|
||||
|
||||
}
|
||||
\seealso{
|
||||
\itemize{
|
||||
\item The download handler, like other outputs, is suspended (disabled) by
|
||||
default for download buttons and links that are hidden. Use
|
||||
\code{\link[=outputOptions]{outputOptions()}} to control this behavior, e.g. to set
|
||||
\code{suspendWhenHidden = FALSE} if the download is initiated by
|
||||
programmatically clicking on the download button using JavaScript.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,7 @@ numericInput(
|
||||
min = NA,
|
||||
max = NA,
|
||||
step = NA,
|
||||
width = NULL,
|
||||
...,
|
||||
updateOn = c("change", "blur")
|
||||
width = NULL
|
||||
)
|
||||
}
|
||||
\arguments{
|
||||
@@ -31,16 +29,6 @@ numericInput(
|
||||
|
||||
\item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
|
||||
see \code{\link[=validateCssUnit]{validateCssUnit()}}.}
|
||||
|
||||
\item{...}{Ignored, included to require named arguments and for future
|
||||
feature expansion.}
|
||||
|
||||
\item{updateOn}{A character vector specifying when the input should be
|
||||
updated. Options are \code{"change"} (default) and \code{"blur"}. Use \code{"change"} to
|
||||
update the input immediately whenever the value changes. Use \code{"blur"}to
|
||||
delay the input update until the input loses focus (the user moves away
|
||||
from the input), or when Enter is pressed (or Cmd/Ctrl + Enter for
|
||||
\code{\link[=textAreaInput]{textAreaInput()}}).}
|
||||
}
|
||||
\value{
|
||||
A numeric input control that can be added to a UI definition.
|
||||
|
||||
@@ -4,15 +4,7 @@
|
||||
\alias{passwordInput}
|
||||
\title{Create a password input control}
|
||||
\usage{
|
||||
passwordInput(
|
||||
inputId,
|
||||
label,
|
||||
value = "",
|
||||
width = NULL,
|
||||
placeholder = NULL,
|
||||
...,
|
||||
updateOn = c("change", "blur")
|
||||
)
|
||||
passwordInput(inputId, label, value = "", width = NULL, placeholder = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
@@ -27,16 +19,6 @@ see \code{\link[=validateCssUnit]{validateCssUnit()}}.}
|
||||
\item{placeholder}{A character string giving the user a hint as to what can
|
||||
be entered into the control. Internet Explorer 8 and 9 do not support this
|
||||
option.}
|
||||
|
||||
\item{...}{Ignored, included to require named arguments and for future
|
||||
feature expansion.}
|
||||
|
||||
\item{updateOn}{A character vector specifying when the input should be
|
||||
updated. Options are \code{"change"} (default) and \code{"blur"}. Use \code{"change"} to
|
||||
update the input immediately whenever the value changes. Use \code{"blur"}to
|
||||
delay the input update until the input loses focus (the user moves away
|
||||
from the input), or when Enter is pressed (or Cmd/Ctrl + Enter for
|
||||
\code{\link[=textAreaInput]{textAreaInput()}}).}
|
||||
}
|
||||
\value{
|
||||
A text input control that can be added to a UI definition.
|
||||
|
||||
@@ -125,9 +125,6 @@ shinyApp(ui, server)
|
||||
}
|
||||
}
|
||||
\seealso{
|
||||
\itemize{
|
||||
\item For more details on how the images are generated, and how to control
|
||||
For more details on how the images are generated, and how to control
|
||||
the output, see \code{\link[=plotPNG]{plotPNG()}}.
|
||||
\item Use \code{\link[=outputOptions]{outputOptions()}} to set general output options for an image output.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,3 @@ vecFun()
|
||||
|
||||
})
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=outputOptions]{outputOptions()}}
|
||||
}
|
||||
|
||||
@@ -52,5 +52,5 @@ shinyApp(ui, server)
|
||||
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=uiOutput]{uiOutput()}}, \code{\link[=outputOptions]{outputOptions()}}
|
||||
\code{\link[=uiOutput]{uiOutput()}}
|
||||
}
|
||||
|
||||
@@ -44,20 +44,16 @@ have the extensions: r, htm, html, js, css, png, jpg, jpeg, gif. If any
|
||||
changes are detected, all connected Shiny sessions are reloaded. This
|
||||
allows for fast feedback loops when tweaking Shiny UI.
|
||||
|
||||
Monitoring for changes is no longer expensive, thanks to the \pkg{watcher}
|
||||
package, but this feature is still intended only for development.
|
||||
Since monitoring for changes is expensive (we simply poll for last
|
||||
modified times), this feature is intended only for development.
|
||||
|
||||
You can customize the file patterns Shiny will monitor by setting the
|
||||
shiny.autoreload.pattern option. For example, to monitor only \code{ui.R}:
|
||||
\code{options(shiny.autoreload.pattern = glob2rx("ui.R"))}.
|
||||
shiny.autoreload.pattern option. For example, to monitor only ui.R:
|
||||
\code{options(shiny.autoreload.pattern = glob2rx("ui.R"))}
|
||||
|
||||
As mentioned above, Shiny no longer polls watched files for changes.
|
||||
Instead, using \pkg{watcher}, Shiny is notified of file changes as they
|
||||
occur. These changes are batched together within a customizable latency
|
||||
period. You can adjust this period by setting
|
||||
\code{options(shiny.autoreload.interval = 2000)} (in milliseconds). This value
|
||||
converted to seconds and passed to the \code{latency} argument of
|
||||
\code{\link[watcher:watcher]{watcher::watcher()}}. The default latency is 250ms.}
|
||||
The default polling interval is 500 milliseconds. You can change this
|
||||
by setting e.g. \code{options(shiny.autoreload.interval = 2000)} (every
|
||||
two seconds).}
|
||||
\item{shiny.deprecation.messages (defaults to \code{TRUE})}{This controls whether messages for
|
||||
deprecated functions in Shiny will be printed. See
|
||||
\code{\link[=shinyDeprecated]{shinyDeprecated()}} for more information.}
|
||||
|
||||
@@ -13,10 +13,7 @@ textAreaInput(
|
||||
cols = NULL,
|
||||
rows = NULL,
|
||||
placeholder = NULL,
|
||||
resize = NULL,
|
||||
...,
|
||||
autoresize = FALSE,
|
||||
updateOn = c("change", "blur")
|
||||
resize = NULL
|
||||
)
|
||||
}
|
||||
\arguments{
|
||||
@@ -49,19 +46,6 @@ option.}
|
||||
\item{resize}{Which directions the textarea box can be resized. Can be one of
|
||||
\code{"both"}, \code{"none"}, \code{"vertical"}, and \code{"horizontal"}. The default, \code{NULL},
|
||||
will use the client browser's default setting for resizing textareas.}
|
||||
|
||||
\item{...}{Ignored, included to require named arguments and for future
|
||||
feature expansion.}
|
||||
|
||||
\item{autoresize}{If \code{TRUE}, the textarea will automatically resize to fit
|
||||
the input text.}
|
||||
|
||||
\item{updateOn}{A character vector specifying when the input should be
|
||||
updated. Options are \code{"change"} (default) and \code{"blur"}. Use \code{"change"} to
|
||||
update the input immediately whenever the value changes. Use \code{"blur"}to
|
||||
delay the input update until the input loses focus (the user moves away
|
||||
from the input), or when Enter is pressed (or Cmd/Ctrl + Enter for
|
||||
\code{\link[=textAreaInput]{textAreaInput()}}).}
|
||||
}
|
||||
\value{
|
||||
A textarea input control that can be added to a UI definition.
|
||||
|
||||
@@ -4,15 +4,7 @@
|
||||
\alias{textInput}
|
||||
\title{Create a text input control}
|
||||
\usage{
|
||||
textInput(
|
||||
inputId,
|
||||
label,
|
||||
value = "",
|
||||
width = NULL,
|
||||
placeholder = NULL,
|
||||
...,
|
||||
updateOn = c("change", "blur")
|
||||
)
|
||||
textInput(inputId, label, value = "", width = NULL, placeholder = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{inputId}{The \code{input} slot that will be used to access the value.}
|
||||
@@ -27,16 +19,6 @@ see \code{\link[=validateCssUnit]{validateCssUnit()}}.}
|
||||
\item{placeholder}{A character string giving the user a hint as to what can
|
||||
be entered into the control. Internet Explorer 8 and 9 do not support this
|
||||
option.}
|
||||
|
||||
\item{...}{Ignored, included to require named arguments and for future
|
||||
feature expansion.}
|
||||
|
||||
\item{updateOn}{A character vector specifying when the input should be
|
||||
updated. Options are \code{"change"} (default) and \code{"blur"}. Use \code{"change"} to
|
||||
update the input immediately whenever the value changes. Use \code{"blur"}to
|
||||
delay the input update until the input loses focus (the user moves away
|
||||
from the input), or when Enter is pressed (or Cmd/Ctrl + Enter for
|
||||
\code{\link[=textAreaInput]{textAreaInput()}}).}
|
||||
}
|
||||
\value{
|
||||
A text input control that can be added to a UI definition.
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"lit": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.3",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/preset-env": "^7.14.2",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@babel/runtime": "^7.14.0",
|
||||
"@deanc/esbuild-plugin-postcss": "^1.0.2",
|
||||
"@selectize/selectize": "https://github.com/selectize/selectize.js.git#e3f2e0b4aa251375bc21b5fcd8ca7d374a921f08",
|
||||
"@testing-library/dom": "^7.31.0",
|
||||
@@ -46,6 +51,7 @@
|
||||
"caniuse-lite": "^1.0.30001312",
|
||||
"core-js": "^3.13.0",
|
||||
"esbuild": "^0.15.10",
|
||||
"esbuild-plugin-babel": "https://github.com/schloerke/esbuild-plugin-babel#patch-2",
|
||||
"esbuild-plugin-globals": "^0.1.1",
|
||||
"esbuild-plugin-sass": "^1.0.1",
|
||||
"eslint": "^8.24.0",
|
||||
|
||||
@@ -150,6 +150,12 @@ All config files are located in the root folder to avoid opening two separate VS
|
||||
* Used by `prettier` to know how to adjust code when a file is saved in VSCode or within `eslint`'s linting process.
|
||||
* `yarnrc.yml`
|
||||
* Notifies `yarn` to use `yarn` v2, install `./node_modules` folder for `esbuild`, and any CLI plugins.
|
||||
* `babel.config.json`
|
||||
* Used within `babel` transpilation of TypeScript -> JavaScript -> polyfilled JavaScript.
|
||||
* Noteable options set:
|
||||
* `"useBuiltIns": "usage"` - `core-js` polyfills are only added as they are _used_.
|
||||
* `"corejs": "3.9"` - This number should match the installed `core-js` number.
|
||||
* `"ignore":["node_modules/core-js"]` - The `core-js` library is directly ignored to [avoid being processed by `babel`](https://github.com/zloirock/core-js/issues/743#issuecomment-571983318).
|
||||
* `jest.config.js`
|
||||
* Used to configure [`jest` testing](https://jestjs.io/)
|
||||
* `package.json`
|
||||
@@ -162,7 +168,7 @@ All config files are located in the root folder to avoid opening two separate VS
|
||||
* `tsconfig.json` -
|
||||
* TypeScript config file
|
||||
* Notable options set:
|
||||
* `target: ES2021` - Compile to es2021.
|
||||
* `target: ES5` - Compile to es5, so babel has an easier job.
|
||||
* `preserveConstEnums: false` - Do no preserve enum values into the final code. (If true, produces bloat / unused code)
|
||||
* `isolatedModules: true` & `esModuleInterop: true` - Requested by `esbuild`. This [allows for `esbuild`](https://esbuild.github.io/content-types/#typescript) to safely compile the files in parallel
|
||||
|
||||
|
||||
108
srcts/TODO.md
Normal file
108
srcts/TODO.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Development Rules
|
||||
* Put Imports at top
|
||||
* Put Exports at bottom
|
||||
* File / Folder structure
|
||||
* Lean towards using more many files / folder vs larger files
|
||||
* Nest folders inside larger _ideas_
|
||||
* Each folder should generally have an `**/index.ts` to export most everything for the folder
|
||||
* Exception: `./src/window` folder. Must call methods directly.
|
||||
* Any `window.***` calls are done in `./src/window` folder only.
|
||||
* Add exported values / function if necessary.
|
||||
* This helps keep each file self contained. Trying not to have random inputs from anywhere
|
||||
* jQuery
|
||||
* Always import `./src/jquery` instead of `"jquery"`
|
||||
* Exeption: Test files. There, you can import `"jquery"` directly as the browser is not available to already have jQuery loaded.
|
||||
* Prevents from installing local jquery in addition to global jquery
|
||||
* WAY to many packages exist on the assumption that jquery is available at run time. Therefore, it can not be removed globally. :-(
|
||||
* Anything that needs to be initialized on start **must** exist in the `./src/initialize` folder.
|
||||
* No file should produce any side effects.
|
||||
* To capture initializations, export a `setFoo(foo_)` method that updates a locally defined `foo` variable.
|
||||
|
||||
|
||||
# TODO
|
||||
|
||||
* √ Move everything into a single ts file. This will allow for the functions to find themselves
|
||||
* √ Except the utils.js already converted
|
||||
* √ es6 shiny.js
|
||||
* √ Pass in version using esbuild
|
||||
* √ Move all shiny files in order to main.ts
|
||||
* √ validate polyfills are working by finding them in the code
|
||||
* √ Produce minified shiny js
|
||||
* √ Disable $ from being found without an import
|
||||
* √ Using a patch with yarn v2
|
||||
* √ Document `./package.json` scripts
|
||||
* √ Verify that `babel` is configurable
|
||||
* √ Use targeting browsers
|
||||
* √ Verify it works on phantomjs / shinytest
|
||||
* √ Set up initial jest tests
|
||||
* √ Use a global shim to avoid importing jquery directly, but make testing easy to test
|
||||
* Update the /tools/update*.R scripts to produce a version and install node dependencies
|
||||
* √ jquery
|
||||
* √ ion range slider
|
||||
* √ selectize
|
||||
* √ strftime
|
||||
* √ bootstrap date picker
|
||||
* font awesome?
|
||||
* bootstrap accessibility plugin?
|
||||
|
||||
# Round #2
|
||||
* Convert registered bindings
|
||||
* √ Input bindings
|
||||
* √ Output bindings
|
||||
* Add default value to `subscribe(callback)` callback function of `false`. B/c if the value was not provided, it was not truthy, therefore equivalent to `false`.
|
||||
* √ radio
|
||||
* √ checkboxgroup
|
||||
* √ daterange
|
||||
* √ actionbutton
|
||||
* √ bootstraptabinput
|
||||
* √ snake_case to camelCase conversions.
|
||||
* √ globally import strftime from `window.strftime`
|
||||
* Remove `evt` from jQuery.on callbacks where `evt` was not used.
|
||||
* √ checkbox.subscribe
|
||||
* √ checkboxgroup.subscribe
|
||||
* √ radio.subscribe
|
||||
* √ slider.subscribe
|
||||
* √ date.subscribe
|
||||
* √ selectInput.subscribe
|
||||
* √ actionButton.subscribe
|
||||
* √ bootstraptabinput.subscribe
|
||||
* Convert usage of `+x` to `Number(x)`
|
||||
* https://stackoverflow.com/a/15872631/591574
|
||||
* √ slider.getValue()
|
||||
* √ number.getValue()
|
||||
* √ Adjust tabinput.ts `setValue()` to return either `false | void`, not `false | true`.
|
||||
* What matters is that `false` is returned, or nothing is returned. Replaced `return true;` with `return;`
|
||||
* Questions
|
||||
* Why does `receiveMessage(data)` sometimes have a `label`?
|
||||
* Should we have a update datatables script?
|
||||
|
||||
|
||||
# Later TODO
|
||||
|
||||
* Use --strictNullChecks in tsconfig.json
|
||||
|
||||
* Make `_*()` methods `private *()`
|
||||
* √ Each _file_ will be pulled out as possible into smaller files in separate PRs
|
||||
* √ Convert `FileProcessor` to a true class definition
|
||||
* Break up `./utils` into many files
|
||||
* √ Remove any `: any` types
|
||||
* √ Make `@typescript-eslint/explicit-module-boundary-types` an error
|
||||
* √ Fix all `// eslint-disable-next-line no-prototype-builtins` lines
|
||||
* TypeScript other shiny files (ex: showcasemode)
|
||||
* √ Completely remove `parcel` from `./package.json` and only use `esbuild`
|
||||
* √ Delete 'shiny-es5' files
|
||||
* Delete 'old' folder
|
||||
* _Uglify_ js files (like in previous Gruntfile.js)
|
||||
* datepicker
|
||||
* ionrangeslider
|
||||
* selectize
|
||||
|
||||
|
||||
|
||||
# Eventual TODO
|
||||
* Use yarn PnP
|
||||
* See `./patch/yarn_pnp.patch`
|
||||
* Use [esbuild](https://github.com/yarnpkg/berry/tree/master/packages/esbuild-plugin-pnp#yarnpkgesbuild-plugin-pnp)
|
||||
* Known problems:
|
||||
* `@yarnpkg/esbuild-plugin-pnp@0.0.1` gives full file paths, not relative file paths
|
||||
* `@testing-library/jest-dom/extend-expect` can not be found.
|
||||
@@ -14,6 +14,10 @@ import process from "process";
|
||||
// @ts-ignore; Type definitions are not found. This occurs when `strict: true` in tsconfig.json
|
||||
import readcontrol from "readcontrol";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore; Type definitions are not found. This occurs when `strict: true` in tsconfig.json
|
||||
import babelPlugin from "esbuild-plugin-babel";
|
||||
|
||||
const outDir = "./inst/www/shared/";
|
||||
|
||||
type ShinyDesc = { version: string; package: string; license: string };
|
||||
@@ -75,7 +79,7 @@ async function build(
|
||||
return esbuildBuild({
|
||||
incremental: incremental,
|
||||
watch: watch,
|
||||
target: "es2021",
|
||||
target: "es5",
|
||||
preserveSymlinks: true,
|
||||
...opts,
|
||||
}).then((x) => {
|
||||
@@ -84,4 +88,4 @@ async function build(
|
||||
});
|
||||
}
|
||||
|
||||
export { outDir, build, shinyDesc, banner };
|
||||
export { outDir, build, shinyDesc, banner, babelPlugin };
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
|
||||
// - TypeScript -----------------------------------------------------------
|
||||
|
||||
import { banner, build, outDir } from "./_build";
|
||||
import { banner, build, outDir, babelPlugin } from "./_build";
|
||||
|
||||
build({
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
minify: true,
|
||||
plugins: [],
|
||||
plugins: [babelPlugin()],
|
||||
banner: banner,
|
||||
entryPoints: [
|
||||
"srcts/extras/shiny-autoreload.ts",
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
// yarn build
|
||||
// ```
|
||||
|
||||
import type { BuildOptions } from "esbuild";
|
||||
import { banner, build, outDir, shinyDesc, babelPlugin } from "./_build";
|
||||
import globalsPlugin from "esbuild-plugin-globals";
|
||||
import { banner, build, outDir, shinyDesc } from "./_build";
|
||||
import type { BuildOptions } from "esbuild";
|
||||
import { verifyJqueryImport } from "./_jquery";
|
||||
|
||||
const opts: BuildOptions = {
|
||||
@@ -18,6 +18,7 @@ const opts: BuildOptions = {
|
||||
//// Loaded dynamically. MUST use `window.strftime` within code
|
||||
// strftime: "window.strftime",
|
||||
}),
|
||||
babelPlugin(),
|
||||
],
|
||||
define: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
.recalculating {
|
||||
|
||||
min-height: var(--shiny-spinner-size, 32px);
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
@@ -47,31 +45,13 @@
|
||||
transition: opacity 250ms ease var(--shiny-spinner-delay, 1s);
|
||||
}
|
||||
|
||||
/*
|
||||
When htmlwidget errors are rendered, an inline `visibility:hidden` is put
|
||||
on the html-widget-output, and the error message (if any) is put in a
|
||||
sibling element that overlays the output container (this way, the height
|
||||
of the output container doesn't change). Work around this by making the
|
||||
output container itself visible and making the children (except the
|
||||
spinner) invisible.
|
||||
*/
|
||||
&.html-widget-output {
|
||||
visibility: inherit !important;
|
||||
> * {
|
||||
visibility: hidden;
|
||||
}
|
||||
::after {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Disable spinner on uiOutput() mainly because (for other reasons) it has
|
||||
`display:contents`, which breaks the ::after positioning.
|
||||
Note that, even if we could position it, we'd probably want to disable it
|
||||
if it has recalculating children.
|
||||
*/
|
||||
&.shiny-html-output:not(.shiny-table-output)::after {
|
||||
&.shiny-html-output::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -125,9 +105,6 @@
|
||||
&.shiny-busy:has(.recalculating:not(.shiny-html-output))::after {
|
||||
display: none;
|
||||
}
|
||||
&.shiny-busy:has(.recalculating.shiny-table-output)::after {
|
||||
display: none;
|
||||
}
|
||||
&.shiny-busy:has(#shiny-disconnected-overlay)::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.textarea-autoresize textarea.form-control {
|
||||
padding: 5px 8px;
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
}
|
||||
9
srcts/patch/README.md
Normal file
9
srcts/patch/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# `yarn` Patch files
|
||||
|
||||
* `yarn_pnp.patch`
|
||||
* This file is currently not used and is outdated.
|
||||
* This provides a good game plan on how to use PnP with Yarn once esbuild can easily be integrated with Yarn PnP.
|
||||
* Using PnP removes the `node_modules` folder, but adds a zip of each package. I **do not** like Yarn's suggestion to commit these zip files to support their [Zero Installs](https://next.yarnpkg.com/features/zero-installs) philosophy.
|
||||
* Reference:
|
||||
* https://next.yarnpkg.com/features/pnp
|
||||
* https://yarnpkg.com/api/modules/esbuild_plugin_pnp.html
|
||||
250
srcts/patch/yarn_pnp.patch
Normal file
250
srcts/patch/yarn_pnp.patch
Normal file
@@ -0,0 +1,250 @@
|
||||
diff --git a/srcts/TODO.md b/srcts/TODO.md
|
||||
index dbe275f1..e36ced9e 100644
|
||||
--- a/srcts/TODO.md
|
||||
+++ b/srcts/TODO.md
|
||||
@@ -36,6 +36,7 @@
|
||||
* √ Verify it works on phantomjs / shinytest
|
||||
* √ Set up initial jest tests
|
||||
* √ Use a global shim to avoid importing jquery directly, but make testing easy to test
|
||||
+* √ Use yarn PnP
|
||||
|
||||
# Later TODO
|
||||
|
||||
@@ -49,12 +50,3 @@
|
||||
* Completely remove `parcel` from `./package.json` and only use `esbuild`
|
||||
* Delete 'shiny-es5' files
|
||||
* Delete 'old' folder
|
||||
-
|
||||
-
|
||||
-# Eventual TODO
|
||||
-* Use yarn PnP
|
||||
- * Use [esbuild](https://github.com/yarnpkg/berry/tree/master/packages/esbuild-plugin-pnp#yarnpkgesbuild-plugin-pnp)
|
||||
- * Remove `./.yarnrc.yaml` `nodeLinker` key
|
||||
- * TODO - Figure out how to call the esbuild command with the missing packages. Currently Yarn can't ifnd `esbuild` and suggests `esbuild-X.Y.Z-SHA` (or something other than `esbuild`) which does not make sense.
|
||||
- * Calling `yarn node esbuild.config.mjs` does not work
|
||||
- * Calling `yarn pnpify node esbuild.config.mjs` does not work
|
||||
diff --git a/srcts/esbuild.config.js b/srcts/esbuild.config.js
|
||||
new file mode 100644
|
||||
index 00000000..8e4a0801
|
||||
--- /dev/null
|
||||
+++ b/srcts/esbuild.config.js
|
||||
@@ -0,0 +1,54 @@
|
||||
+/* eslint-disable no-undef */
|
||||
+/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
+
|
||||
+// !! Do not convert this file to a module (aka using `import` statements) as VSCode suggests to do
|
||||
+// Yarn shims the `require` function to make PnP work. It does not work on import statements
|
||||
+
|
||||
+const esbuild = require("esbuild");
|
||||
+const babel = require("esbuild-plugin-babel");
|
||||
+const readcontrol = require("readcontrol");
|
||||
+const pnpPlugin = require("esbuild-plugin-pnp");
|
||||
+const process = require("process");
|
||||
+const globalsPlugin = require("esbuild-plugin-globals");
|
||||
+
|
||||
+let watch = process.argv.length >= 3 && process.argv[2] == "--watch";
|
||||
+
|
||||
+let outdir = "../inst/www/shared/";
|
||||
+let opts = {
|
||||
+ entryPoints: ["src/index.ts"],
|
||||
+ bundle: true,
|
||||
+ watch: watch,
|
||||
+ plugins: [
|
||||
+ globalsPlugin({
|
||||
+ jquery: "window.jQuery",
|
||||
+ }),
|
||||
+ pnpPlugin(),
|
||||
+ babel(),
|
||||
+ ],
|
||||
+ target: "es5",
|
||||
+ sourcemap: true,
|
||||
+ define: {
|
||||
+ "process.env.SHINY_VERSION": `"${
|
||||
+ readcontrol.readSync("../DESCRIPTION").version
|
||||
+ }"`,
|
||||
+ },
|
||||
+};
|
||||
+
|
||||
+console.log("Building shiny.js");
|
||||
+esbuild
|
||||
+ .build({
|
||||
+ ...opts,
|
||||
+ outfile: outdir + "shiny.js",
|
||||
+ })
|
||||
+ .then(() => {
|
||||
+ console.log("Building shiny.min.js");
|
||||
+ esbuild.build({
|
||||
+ ...opts,
|
||||
+ outfile: outdir + "shiny.min.js",
|
||||
+ minify: true,
|
||||
+ });
|
||||
+ })
|
||||
+ .catch((reason) => {
|
||||
+ console.error(reason);
|
||||
+ process.exit(1);
|
||||
+ });
|
||||
diff --git a/srcts/esbuild.config.mjs b/srcts/esbuild.config.mjs
|
||||
deleted file mode 100644
|
||||
index ffdb855b..00000000
|
||||
--- a/srcts/esbuild.config.mjs
|
||||
+++ /dev/null
|
||||
@@ -1,40 +0,0 @@
|
||||
-import esbuild from "esbuild";
|
||||
-import babel from "esbuild-plugin-babel";
|
||||
-import readcontrol from "readcontrol";
|
||||
-import process from "process";
|
||||
-import globalsPlugin from "esbuild-plugin-globals";
|
||||
-
|
||||
-let watch = process.argv.length >= 3 && process.argv[2] == "--watch";
|
||||
-
|
||||
-let outdir = "../inst/www/shared/";
|
||||
-let opts = {
|
||||
- entryPoints: ["src/index.ts"],
|
||||
- bundle: true,
|
||||
- watch: watch,
|
||||
- plugins: [
|
||||
- globalsPlugin({
|
||||
- jquery: "window.jQuery",
|
||||
- }),
|
||||
- babel(),
|
||||
- ],
|
||||
- target: "es5",
|
||||
- sourcemap: true,
|
||||
- define: {
|
||||
- "process.env.SHINY_VERSION": `"${
|
||||
- readcontrol.readSync("../DESCRIPTION").version
|
||||
- }"`,
|
||||
- },
|
||||
-};
|
||||
-
|
||||
-console.log("Building shiny.js");
|
||||
-await esbuild.build({
|
||||
- ...opts,
|
||||
- outfile: outdir + "shiny.js",
|
||||
-});
|
||||
-
|
||||
-console.log("Building shiny.min.js");
|
||||
-await esbuild.build({
|
||||
- ...opts,
|
||||
- outfile: outdir + "shiny.min.js",
|
||||
- minify: true,
|
||||
-});
|
||||
diff --git a/srcts/package.json b/srcts/package.json
|
||||
index c7f6b66b..edff5ce9 100644
|
||||
--- a/srcts/package.json
|
||||
+++ b/srcts/package.json
|
||||
@@ -21,8 +21,9 @@
|
||||
"@typescript-eslint/parser": "^4",
|
||||
"browserslist": "^4.16.3",
|
||||
"esbuild": "^0.8.50",
|
||||
- "esbuild-plugin-babel": "0.2.3",
|
||||
+ "esbuild-plugin-babel": "patch:esbuild-plugin-babel@0.2.3#./patch/esbuild-plugin-babel.patch",
|
||||
"esbuild-plugin-globals": "^0.1.1",
|
||||
+ "esbuild-plugin-pnp": "^0.3.0",
|
||||
"eslint": "^7",
|
||||
"eslint-config-prettier": "^7",
|
||||
"eslint-plugin-jest": "^24",
|
||||
@@ -45,7 +46,7 @@
|
||||
"build": "yarn run build_shiny",
|
||||
"setup_build_shiny": "yarn run lint && yarn run typescript-check",
|
||||
"build_shiny": "yarn run setup_build_shiny && yarn run bundle_shiny",
|
||||
- "bundle_shiny": "node esbuild.config.mjs",
|
||||
+ "bundle_shiny": "node esbuild.config.js",
|
||||
"bundle_shiny_parcel2": "parcel build -d ../inst/www/shared --no-minify -o shiny.js src/index.ts",
|
||||
"watch_parcel2": "yarn run setup_build_shiny && parcel run -d ../inst/www/shared -o shiny.js srcjs/index.ts",
|
||||
"replace_shiny_version2": "replace --silent '\"[^\"]+\"; // @VERSION@' \"\\\"`node -e 'console.log(require(\"readcontrol\").readSync(\"../DESCRIPTION\").version)'`\\\"; // @VERSION@\" src/shiny.ts",
|
||||
diff --git a/srcts/patch/esbuild-plugin-babel.patch b/srcts/patch/esbuild-plugin-babel.patch
|
||||
new file mode 100644
|
||||
index 00000000..24cc9425
|
||||
--- /dev/null
|
||||
+++ b/srcts/patch/esbuild-plugin-babel.patch
|
||||
@@ -0,0 +1,33 @@
|
||||
+diff --git a/package.json b/package.json
|
||||
+index 6b9cdb89e2bbf0f5b5ad65adb53951d129f265ca..4e9b0e08e1c9850ff637eb6a47b5f3cfa33bbb55 100644
|
||||
+--- a/package.json
|
||||
++++ b/package.json
|
||||
+@@ -7,7 +7,6 @@
|
||||
+ "license": "ISC",
|
||||
+ "exports": "./src/index.js",
|
||||
+ "main": "src/index.js",
|
||||
+- "type": "module",
|
||||
+ "scripts": {
|
||||
+ "format": "prettier --write --ignore-unknown '**/*'"
|
||||
+ },
|
||||
+diff --git a/src/index.js b/src/index.js
|
||||
+index b3cff90a292daa6cfac0566ee73bab142ac627df..d06ecf8873a45a5ea3cda7edb3814e8034efed32 100644
|
||||
+--- a/src/index.js
|
||||
++++ b/src/index.js
|
||||
+@@ -1,6 +1,7 @@
|
||||
+-import babel from '@babel/core';
|
||||
+-import fs from 'fs';
|
||||
+-import path from 'path';
|
||||
++const babel = require('@babel/core');
|
||||
++const fs = require('fs');
|
||||
++const path = require('path');
|
||||
++
|
||||
+
|
||||
+ const pluginBabel = (options = {}) => ({
|
||||
+ name: 'babel',
|
||||
+@@ -41,4 +42,4 @@ const pluginBabel = (options = {}) => ({
|
||||
+ }
|
||||
+ });
|
||||
+
|
||||
+-export default pluginBabel;
|
||||
++module.exports = pluginBabel;
|
||||
diff --git a/srcts/yarn.lock b/srcts/yarn.lock
|
||||
index 90d3fd04..1ca29eba 100644
|
||||
--- a/srcts/yarn.lock
|
||||
+++ b/srcts/yarn.lock
|
||||
@@ -4334,7 +4334,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
-"esbuild-plugin-babel@npm:0.2.3":
|
||||
+esbuild-plugin-babel@0.2.3:
|
||||
version: 0.2.3
|
||||
resolution: "esbuild-plugin-babel@npm:0.2.3"
|
||||
peerDependencies:
|
||||
@@ -4343,6 +4343,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
+"esbuild-plugin-babel@patch:esbuild-plugin-babel@0.2.3#./patch/esbuild-plugin-babel.patch::locator=root-workspace-0b6124%40workspace%3A.":
|
||||
+ version: 0.2.3
|
||||
+ resolution: "esbuild-plugin-babel@patch:esbuild-plugin-babel@npm%3A0.2.3#./patch/esbuild-plugin-babel.patch::version=0.2.3&hash=80b9d8&locator=root-workspace-0b6124%40workspace%3A."
|
||||
+ peerDependencies:
|
||||
+ "@babel/core": ^7.0.0
|
||||
+ checksum: 91e0a233ed255b4798b3a1d9b2d9fbc8ea3c107561c69b31790f02c556a4687770a13d2b4c58f3dc638198bcddd5c6d7d26496a6578da730cd635dea1dd8450c
|
||||
+ languageName: node
|
||||
+ linkType: hard
|
||||
+
|
||||
"esbuild-plugin-globals@npm:^0.1.1":
|
||||
version: 0.1.1
|
||||
resolution: "esbuild-plugin-globals@npm:0.1.1"
|
||||
@@ -4350,6 +4359,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
+"esbuild-plugin-pnp@npm:^0.3.0":
|
||||
+ version: 0.3.0
|
||||
+ resolution: "esbuild-plugin-pnp@npm:0.3.0"
|
||||
+ peerDependencies:
|
||||
+ esbuild: ^0.8.1
|
||||
+ checksum: b80ab17bea35ab6eaf9a9adc14c52667c0a5e2c7c8c8e97194c57feb7d14b7247228745a3ad34f69447d6c5241081f38b9786948b879bf0051a479ce74450edc
|
||||
+ languageName: node
|
||||
+ linkType: hard
|
||||
+
|
||||
"esbuild@npm:^0.8.50":
|
||||
version: 0.8.50
|
||||
resolution: "esbuild@npm:0.8.50"
|
||||
@@ -9295,8 +9313,9 @@ fsevents@^2.1.2:
|
||||
browserslist: ^4.16.3
|
||||
core-js: ^3.9
|
||||
esbuild: ^0.8.50
|
||||
- esbuild-plugin-babel: 0.2.3
|
||||
+ esbuild-plugin-babel: "patch:esbuild-plugin-babel@0.2.3#./patch/esbuild-plugin-babel.patch"
|
||||
esbuild-plugin-globals: ^0.1.1
|
||||
+ esbuild-plugin-pnp: ^0.3.0
|
||||
eslint: ^7
|
||||
eslint-config-prettier: ^7
|
||||
eslint-plugin-jest: ^24
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { EventPriority } from "../../inputPolicies/inputPolicy";
|
||||
import type { RatePolicyModes } from "../../inputPolicies/inputRateDecorator";
|
||||
import type { BindScope } from "../../shiny/bind";
|
||||
|
||||
@@ -27,16 +26,10 @@ class InputBinding {
|
||||
el; // unused var
|
||||
}
|
||||
|
||||
// Historically, the callback value could only be boolean. In this case:
|
||||
// * false: send value immediately (i.e., priority = "immediate")
|
||||
// * true: send value later (i.e., priority = "deferred")
|
||||
// * The input rate policy is also consulted on whether to debounce or throttle
|
||||
// In recent versions, the value can also be "event", meaning that the
|
||||
// value should be sent regardless of whether it has changed.
|
||||
subscribe(
|
||||
el: HTMLElement,
|
||||
callback: (value: EventPriority | boolean) => void
|
||||
): void {
|
||||
// The callback method takes one argument, whose value is boolean. If true,
|
||||
// allow deferred (debounce or throttle) sending depending on the value of
|
||||
// getRatePolicy. If false, send value immediately. Default behavior is `false`
|
||||
subscribe(el: HTMLElement, callback: (value: boolean) => void): void {
|
||||
// empty
|
||||
el; // unused var
|
||||
callback; // unused var
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import { $escape, hasDefinedProperty, updateLabel } from "../../utils";
|
||||
|
||||
import type { EventPriority } from "../../inputPolicies/inputPolicy";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
|
||||
// interface TextHTMLElement extends NameValueHTMLElement {
|
||||
@@ -50,42 +49,22 @@ class TextInputBindingBase extends InputBinding {
|
||||
value;
|
||||
}
|
||||
|
||||
subscribe(
|
||||
el: TextHTMLElement,
|
||||
callback: (x: EventPriority | boolean) => void
|
||||
): void {
|
||||
const $el = $(el);
|
||||
const updateOn = $el.data("update-on") || "change";
|
||||
|
||||
if (updateOn === "change") {
|
||||
$el.on(
|
||||
"keyup.textInputBinding input.textInputBinding",
|
||||
// event: Event
|
||||
function () {
|
||||
callback(true);
|
||||
}
|
||||
);
|
||||
} else if (updateOn === "blur") {
|
||||
$el.on("blur.textInputBinding", function () {
|
||||
callback(false);
|
||||
});
|
||||
$el.on("keydown.textInputBinding", function (event: JQuery.Event) {
|
||||
if (event.key !== "Enter") return;
|
||||
if ($el.is("textarea")) {
|
||||
if (!(event.ctrlKey || event.metaKey)) return;
|
||||
}
|
||||
callback(false);
|
||||
});
|
||||
}
|
||||
|
||||
$el.on("change.textInputBinding", function () {
|
||||
if (updateOn === "blur" && $el.is(":focus")) {
|
||||
return;
|
||||
subscribe(el: TextHTMLElement, callback: (x: boolean) => void): void {
|
||||
$(el).on(
|
||||
"keyup.textInputBinding input.textInputBinding",
|
||||
// event: Event
|
||||
function () {
|
||||
callback(true);
|
||||
}
|
||||
callback(false);
|
||||
});
|
||||
);
|
||||
$(el).on(
|
||||
"change.textInputBinding",
|
||||
// event: Event
|
||||
function () {
|
||||
callback(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
unsubscribe(el: TextHTMLElement): void {
|
||||
$(el).off(".textInputBinding");
|
||||
}
|
||||
|
||||
@@ -2,96 +2,11 @@ import $ from "jquery";
|
||||
|
||||
import { TextInputBinding } from "./text";
|
||||
|
||||
export class TextareaInputBinding extends TextInputBinding {
|
||||
class TextareaInputBinding extends TextInputBinding {
|
||||
find(scope: HTMLElement): JQuery<HTMLElement> {
|
||||
// Inputs now also have the .shiny-input-textarea class
|
||||
return $(scope).find("textarea");
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* Code below this point is for textAreaInput(autoresize=TRUE)
|
||||
************************************************************/
|
||||
|
||||
interface DOMEvent<T extends EventTarget> extends Event {
|
||||
readonly target: T;
|
||||
}
|
||||
|
||||
function onDelegatedEvent(
|
||||
eventName: string,
|
||||
selector: string,
|
||||
callback: (target: HTMLTextAreaElement) => void
|
||||
) {
|
||||
document.addEventListener(eventName, (e) => {
|
||||
const e2 = e as DOMEvent<HTMLTextAreaElement>;
|
||||
if (e2.target.matches(selector)) {
|
||||
callback(e2.target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Use a single intersectionObserver as they are slow to create / use.
|
||||
let textAreaIntersectionObserver: IntersectionObserver | null = null;
|
||||
|
||||
function callUpdateHeightWhenTargetIsVisible(target: HTMLTextAreaElement) {
|
||||
if (textAreaIntersectionObserver === null) {
|
||||
// Create a single observer to watch for the textarea becoming visible
|
||||
textAreaIntersectionObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
// Quit if the entry is not visible
|
||||
if (!entry.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
// If the entry is visible (even if it's just a single pixel)
|
||||
// Stop observing the target
|
||||
textAreaIntersectionObserver?.unobserve(entry.target);
|
||||
|
||||
// Update the height of the textarea
|
||||
updateHeight(entry.target as HTMLTextAreaElement);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
textAreaIntersectionObserver.observe(target);
|
||||
}
|
||||
|
||||
function updateHeight(target: HTMLTextAreaElement) {
|
||||
if (target.scrollHeight > 0) {
|
||||
// Automatically resize the textarea to fit its content.
|
||||
target.style.height = "auto";
|
||||
target.style.height = target.scrollHeight + "px";
|
||||
} else {
|
||||
// The textarea is not visible on the page, therefore it has a 0 scroll height.
|
||||
|
||||
// If we should autoresize the text area height, then we can wait for the textarea to
|
||||
// become visible and call `updateHeight` again. Hopefully the scroll height is no
|
||||
// longer 0
|
||||
callUpdateHeightWhenTargetIsVisible(target);
|
||||
}
|
||||
}
|
||||
|
||||
// Update on change
|
||||
onDelegatedEvent(
|
||||
"input",
|
||||
"textarea.textarea-autoresize",
|
||||
(target: HTMLTextAreaElement) => {
|
||||
updateHeight(target);
|
||||
}
|
||||
);
|
||||
|
||||
// Update on load
|
||||
function updateOnLoad() {
|
||||
if (document.readyState === "loading") {
|
||||
// Document still loading, wait 10ms to check again.
|
||||
setTimeout(updateOnLoad, 10);
|
||||
return;
|
||||
}
|
||||
|
||||
// document.readyState in ["interactive", "complete"];\
|
||||
const textAreas = document.querySelectorAll(
|
||||
"textarea.textarea-autoresize"
|
||||
) as NodeListOf<HTMLTextAreaElement>;
|
||||
textAreas.forEach(updateHeight);
|
||||
}
|
||||
|
||||
updateOnLoad();
|
||||
export { TextareaInputBinding };
|
||||
|
||||
@@ -200,42 +200,6 @@ class ShinyErrorConsole extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
static createClientMessageElement({ headline, message }: ShinyClientMessage) {
|
||||
const msg = document.createElement("shiny-error-message");
|
||||
msg.setAttribute("headline", headline || "");
|
||||
msg.setAttribute("message", message);
|
||||
return msg;
|
||||
}
|
||||
|
||||
appendConsoleMessage({ headline, message }: ShinyClientMessage) {
|
||||
const content =
|
||||
this.shadowRoot?.querySelector<HTMLSlotElement>("slot.content");
|
||||
|
||||
if (content) {
|
||||
const nodeKey = (node: Element) => {
|
||||
const headline = node.getAttribute("headline") || "";
|
||||
const message = node.getAttribute("message") || "";
|
||||
return `${headline}::${message}`;
|
||||
};
|
||||
const newKey = `${headline}::${message}`;
|
||||
|
||||
for (const node of content.assignedElements()) {
|
||||
if (node.tagName.toLowerCase() === "shiny-error-message") {
|
||||
if (nodeKey(node) === newKey) {
|
||||
// Do nothing, this message is already in the console
|
||||
// TODO: Increase count of message here
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.appendChild(
|
||||
ShinyErrorConsole.createClientMessageElement({ headline, message })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <div class="header">
|
||||
<span class="title"> Shiny Client Errors </span>
|
||||
@@ -559,13 +523,17 @@ function showShinyClientMessage({
|
||||
|
||||
// Check to see if an Error Console Container element already exists. If it
|
||||
// doesn't we need to add it before putting an error on the screen
|
||||
let sec = document.querySelector<ShinyErrorConsole>("shiny-error-console");
|
||||
if (!sec) {
|
||||
sec = document.createElement("shiny-error-console") as ShinyErrorConsole;
|
||||
document.body.appendChild(sec);
|
||||
let errorConsoleContainer = document.querySelector("shiny-error-console");
|
||||
if (!errorConsoleContainer) {
|
||||
errorConsoleContainer = document.createElement("shiny-error-console");
|
||||
document.body.appendChild(errorConsoleContainer);
|
||||
}
|
||||
|
||||
sec.appendConsoleMessage({ headline, message });
|
||||
const errorConsole = document.createElement("shiny-error-message");
|
||||
errorConsole.setAttribute("headline", headline);
|
||||
errorConsole.setAttribute("message", message);
|
||||
|
||||
errorConsoleContainer.appendChild(errorConsole);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
InputRateDecorator,
|
||||
InputValidateDecorator,
|
||||
} from "../inputPolicies";
|
||||
import type { InputPolicyOpts } from "../inputPolicies/inputPolicy";
|
||||
import { shinyAppBindOutput, shinyAppUnbindOutput } from "./initedMethods";
|
||||
import { sendImageSizeFns } from "./sendImageSize";
|
||||
|
||||
@@ -20,7 +19,10 @@ type BindScope = HTMLElement | JQuery<HTMLElement>;
|
||||
* @returns A type predicate indicating if the value is a jQuery<HTMLElement>
|
||||
*/
|
||||
function isJQuery<T = HTMLElement>(value: unknown): value is JQuery<T> {
|
||||
return Boolean(value && (value as any).jquery);
|
||||
return (
|
||||
Object.prototype.hasOwnProperty.call(value, "jquery") &&
|
||||
typeof (value as any).jquery === "string"
|
||||
);
|
||||
}
|
||||
|
||||
// todo make sure allowDeferred can NOT be supplied and still work
|
||||
@@ -28,7 +30,7 @@ function valueChangeCallback(
|
||||
inputs: InputValidateDecorator,
|
||||
binding: InputBinding,
|
||||
el: HTMLElement,
|
||||
priority: InputPolicyOpts["priority"]
|
||||
allowDeferred: boolean
|
||||
) {
|
||||
let id = binding.getId(el);
|
||||
|
||||
@@ -38,7 +40,17 @@ function valueChangeCallback(
|
||||
|
||||
if (type) id = id + ":" + type;
|
||||
|
||||
inputs.setInput(id, value, { priority, binding, el });
|
||||
const opts: {
|
||||
priority: "deferred" | "immediate";
|
||||
binding: typeof binding;
|
||||
el: typeof el;
|
||||
} = {
|
||||
priority: allowDeferred ? "deferred" : "immediate",
|
||||
binding: binding,
|
||||
el: el,
|
||||
};
|
||||
|
||||
inputs.setInput(id, value, opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,17 +275,8 @@ function bindInputs(
|
||||
const thisBinding = binding;
|
||||
const thisEl = el;
|
||||
|
||||
// Historically speaking, this callback has only accepted a boolean value,
|
||||
// but in recent versions it can also accept a input priority.
|
||||
return function (priority: InputPolicyOpts["priority"] | boolean) {
|
||||
const normalizedPriority =
|
||||
typeof priority !== "boolean"
|
||||
? priority
|
||||
: priority
|
||||
? "deferred"
|
||||
: "immediate";
|
||||
|
||||
valueChangeCallback(inputs, thisBinding, thisEl, normalizedPriority);
|
||||
return function (allowDeferred: boolean) {
|
||||
valueChangeCallback(inputs, thisBinding, thisEl, allowDeferred);
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -177,12 +177,12 @@ class ShinyClass {
|
||||
|
||||
let target: InputPolicy;
|
||||
|
||||
if (document.querySelector(".submit-button")) {
|
||||
if ($('input[type="submit"], button[type="submit"]').length > 0) {
|
||||
// If there is a submit button on the page, use defer decorator
|
||||
target = inputsDefer;
|
||||
|
||||
document.querySelectorAll(".submit-button").forEach(function (x) {
|
||||
x.addEventListener("click", function (event) {
|
||||
$('input[type="submit"], button[type="submit"]').each(function () {
|
||||
$(this).click(function (event) {
|
||||
event.preventDefault();
|
||||
inputsDefer.submit();
|
||||
});
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
getShinyCreateWebsocket,
|
||||
getShinyOnCustomMessage,
|
||||
setShinyUser,
|
||||
shinyBindAll,
|
||||
shinyForgetLastInputValue,
|
||||
shinyUnbindAll,
|
||||
} from "./initedMethods";
|
||||
@@ -1054,13 +1053,14 @@ class ShinyApp {
|
||||
const $tabContent = getTabContent($tabset);
|
||||
let tabsetId = $parentTabset.attr("data-tabsetid");
|
||||
|
||||
// Create a virtual element where we'll temporarily hold the rendered
|
||||
// nav controls so we can rewrite some attributes and choose where to
|
||||
// insert the new controls.
|
||||
const $fragLi = $("<div>");
|
||||
await renderContentAsync($fragLi, message.liTag, "afterBegin");
|
||||
|
||||
const $liTag = $($fragLi).find("> li");
|
||||
// TODO: Only render tab content HTML once
|
||||
// The following lines turn the content/nav control HTML into DOM nodes,
|
||||
// but we don't insert these directly, instead we take the HTML from
|
||||
// these nodes and pass it through `renderContentAsync()`. This means
|
||||
// the inserted HTML may not perfectly match the message HTML, esp. if
|
||||
// the content uses web components that alter their HTML when loaded.
|
||||
const $divTag = $(message.divTag.html);
|
||||
const $liTag = $(message.liTag.html);
|
||||
const $aTag = $liTag.find("> a");
|
||||
|
||||
// Unless the item is being prepended/appended, the target tab
|
||||
@@ -1103,16 +1103,13 @@ class ShinyApp {
|
||||
// text items (which function as dividers and headers inside
|
||||
// navbarMenus) and whole navbarMenus (since those get
|
||||
// constructed from scratch on the R side and therefore
|
||||
// there are no ids that need matching). In other words, we're
|
||||
// guaranteed to be inserting only one `nav_panel()`.
|
||||
let fixupDivId = "";
|
||||
// there are no ids that need matching)
|
||||
if ($aTag.attr("data-toggle") === "tab") {
|
||||
const index = getTabIndex($tabset, tabsetId);
|
||||
const tabId = "tab-" + tabsetId + "-" + index;
|
||||
|
||||
$liTag.find("> a").attr("href", "#" + tabId);
|
||||
// We'll fixup the div ID after we insert it
|
||||
fixupDivId = tabId;
|
||||
$divTag.attr("id", tabId);
|
||||
}
|
||||
|
||||
// actually insert the item into the right place
|
||||
@@ -1129,8 +1126,11 @@ class ShinyApp {
|
||||
$tabset.append($liTag);
|
||||
}
|
||||
}
|
||||
await shinyBindAll($targetLiTag?.parent() || $tabset);
|
||||
|
||||
await renderContentAsync($liTag[0], {
|
||||
html: $liTag.html(),
|
||||
deps: message.liTag.deps,
|
||||
});
|
||||
// jcheng 2017-07-28: This next part might look a little insane versus the
|
||||
// more obvious `$tabContent.append($divTag);`, but there's a method to the
|
||||
// madness.
|
||||
@@ -1158,30 +1158,42 @@ class ShinyApp {
|
||||
// In theory the same problem exists for $liTag but since that content is
|
||||
// much less likely to include arbitrary scripts, we're skipping it.
|
||||
//
|
||||
// garrick 2025-01-23: Keeping in mind the above, the `shiny-insert-tab`
|
||||
// method was re-written to avoid adding the nav controls (liTag) and
|
||||
// the nav panel contents (divTag) twice. Now, we use
|
||||
// renderContentAsync() to add both sections to the DOM only once.
|
||||
|
||||
await renderContentAsync($tabContent[0], message.divTag, "beforeEnd");
|
||||
|
||||
if (fixupDivId) {
|
||||
// We're inserting one nav_panel() and need to fixup the content ID
|
||||
$tabContent.find('[id="tab-tsid-id"]').attr("id", fixupDivId);
|
||||
// This code could be nicer if we didn't use renderContent, but rather the
|
||||
// lower-level functions that renderContent uses. Like if we pre-process
|
||||
// the value of message.divTag.html for singletons, we could do that, then
|
||||
// render dependencies, then do $tabContent.append($divTag).
|
||||
await renderContentAsync(
|
||||
$tabContent[0],
|
||||
{ html: "", deps: message.divTag.deps },
|
||||
// @ts-expect-error; TODO-barret; There is no usage of beforeend
|
||||
"beforeend"
|
||||
);
|
||||
for (const el of $divTag.get()) {
|
||||
// Must not use jQuery for appending el to the doc, we don't want any
|
||||
// scripts to run (since they will run when renderContent takes a crack).
|
||||
$tabContent[0].appendChild(el);
|
||||
if (el.nodeType === Node.ELEMENT_NODE) {
|
||||
// If `el` itself is a script tag, this approach won't work (the script
|
||||
// won't be run), since we're only sending innerHTML through renderContent
|
||||
// and not the whole tag. That's fine in this case because we control the
|
||||
// R code that generates this HTML, and we know that the element is not
|
||||
// a script tag.
|
||||
await renderContentAsync(el, el.innerHTML || el.textContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.select) {
|
||||
$liTag.find("a").tab("show");
|
||||
}
|
||||
/* Barbara -- August 2017
|
||||
* Note: until now, the number of tabs in a tabsetPanel (or navbarPage
|
||||
* or navlistPanel) was always fixed. So, an easy way to give an id to
|
||||
* a tab was simply incrementing a counter. (Just like it was easy to
|
||||
* give a random 4-digit number to identify the tabsetPanel). Now that
|
||||
* we're introducing dynamic tabs, we must retrieve these numbers and
|
||||
* fix the dummy id given to the tab in the R side -- there, we always
|
||||
* set the tab id (counter dummy) to "id" and the tabset id to "tsid")
|
||||
*/
|
||||
Note: until now, the number of tabs in a tabsetPanel (or navbarPage
|
||||
or navlistPanel) was always fixed. So, an easy way to give an id to
|
||||
a tab was simply incrementing a counter. (Just like it was easy to
|
||||
give a random 4-digit number to identify the tabsetPanel). Now that
|
||||
we're introducing dynamic tabs, we must retrieve these numbers and
|
||||
fix the dummy id given to the tab in the R side -- there, we always
|
||||
set the tab id (counter dummy) to "id" and the tabset id to "tsid")
|
||||
*/
|
||||
function getTabIndex(
|
||||
$tabset: JQuery<HTMLElement>,
|
||||
tabsetId: string | undefined
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { EventPriority } from "../../inputPolicies/inputPolicy";
|
||||
import type { RatePolicyModes } from "../../inputPolicies/inputRateDecorator";
|
||||
import type { BindScope } from "../../shiny/bind";
|
||||
declare class InputBinding {
|
||||
@@ -7,7 +6,7 @@ declare class InputBinding {
|
||||
getId(el: HTMLElement): string;
|
||||
getType(el: HTMLElement): string | null;
|
||||
getValue(el: HTMLElement): any;
|
||||
subscribe(el: HTMLElement, callback: (value: EventPriority | boolean) => void): void;
|
||||
subscribe(el: HTMLElement, callback: (value: boolean) => void): void;
|
||||
unsubscribe(el: HTMLElement): void;
|
||||
receiveMessage(el: HTMLElement, data: unknown): Promise<void> | void;
|
||||
getState(el: HTMLElement): unknown;
|
||||
|
||||
3
srcts/types/src/bindings/input/text.d.ts
vendored
3
srcts/types/src/bindings/input/text.d.ts
vendored
@@ -1,4 +1,3 @@
|
||||
import type { EventPriority } from "../../inputPolicies/inputPolicy";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
type TextHTMLElement = HTMLInputElement;
|
||||
type TextReceiveMessageData = {
|
||||
@@ -11,7 +10,7 @@ declare class TextInputBindingBase extends InputBinding {
|
||||
getId(el: TextHTMLElement): string;
|
||||
getValue(el: TextHTMLElement): unknown;
|
||||
setValue(el: TextHTMLElement, value: unknown): void;
|
||||
subscribe(el: TextHTMLElement, callback: (x: EventPriority | boolean) => void): void;
|
||||
subscribe(el: TextHTMLElement, callback: (x: boolean) => void): void;
|
||||
unsubscribe(el: TextHTMLElement): void;
|
||||
receiveMessage(el: TextHTMLElement, data: unknown): void;
|
||||
getState(el: TextHTMLElement): unknown;
|
||||
|
||||
3
srcts/types/src/bindings/input/textarea.d.ts
vendored
3
srcts/types/src/bindings/input/textarea.d.ts
vendored
@@ -1,4 +1,5 @@
|
||||
import { TextInputBinding } from "./text";
|
||||
export declare class TextareaInputBinding extends TextInputBinding {
|
||||
declare class TextareaInputBinding extends TextInputBinding {
|
||||
find(scope: HTMLElement): JQuery<HTMLElement>;
|
||||
}
|
||||
export { TextareaInputBinding };
|
||||
|
||||
18
srcts/types/src/bindings/input/textsubmit.d.ts
vendored
18
srcts/types/src/bindings/input/textsubmit.d.ts
vendored
@@ -1,18 +0,0 @@
|
||||
import type { EventPriority } from "../../inputPolicies/inputPolicy";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
type TextHTMLElement = HTMLInputElement;
|
||||
type TextSubmitReceiveMessageData = {
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
submit?: boolean;
|
||||
focus?: boolean;
|
||||
};
|
||||
declare class TextSubmitInputBinding extends InputBinding {
|
||||
find(scope: HTMLElement): JQuery<HTMLElement>;
|
||||
getValue(el: TextHTMLElement): string;
|
||||
setValue(el: TextHTMLElement, value: string): void;
|
||||
subscribe(el: TextHTMLElement, callback: (x: EventPriority | boolean) => void): void;
|
||||
unsubscribe(el: HTMLElement): void;
|
||||
receiveMessage(el: TextHTMLElement, data: TextSubmitReceiveMessageData): void;
|
||||
}
|
||||
export { TextSubmitInputBinding };
|
||||
@@ -183,7 +183,7 @@
|
||||
nav_page
|
||||
Output
|
||||
<body class="bslib-page-navbar">
|
||||
<nav class="navbar navbar-default navbar-static-top" role="navigation" data-bs-theme="light">
|
||||
<nav class="navbar navbar-default navbar-static-top" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<span class="navbar-brand">Title</span>
|
||||
@@ -227,7 +227,7 @@
|
||||
bslib_tags(x)
|
||||
Output
|
||||
<body class="bslib-page-navbar">
|
||||
<nav class="navbar navbar-default navbar-static-top" role="navigation" data-bs-theme="light">
|
||||
<nav class="navbar navbar-default navbar-static-top" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<span class="navbar-brand">Title</span>
|
||||
|
||||
@@ -21,11 +21,10 @@ test_that("Repeated names for selectInput and radioButtons choices", {
|
||||
|
||||
# Select input
|
||||
x <- selectInput('id','label', choices = c(a='x1', a='x2', b='x3'), selectize = FALSE)
|
||||
expect_match(
|
||||
format(x),
|
||||
expect_true(grepl(fixed = TRUE,
|
||||
'<select class="shiny-input-select form-control" id="id"><option value="x1" selected>a</option>\n<option value="x2">a</option>\n<option value="x3">b</option></select>',
|
||||
fixed = TRUE
|
||||
)
|
||||
format(x)
|
||||
))
|
||||
|
||||
# Radio buttons using choices
|
||||
x <- radioButtons('id','label', choices = c(a='x1', a='x2', b='x3'))
|
||||
@@ -249,11 +248,10 @@ test_that("selectInput selects items by default", {
|
||||
))
|
||||
|
||||
# Nothing selected when choices=NULL
|
||||
expect_match(
|
||||
format(selectInput('x', NULL, NULL, selectize = FALSE)),
|
||||
expect_true(grepl(fixed = TRUE,
|
||||
'<select class="shiny-input-select form-control" id="x"></select>',
|
||||
fixed = TRUE
|
||||
)
|
||||
format(selectInput('x', NULL, NULL, selectize = FALSE))
|
||||
))
|
||||
|
||||
# None specified as selected. With multiple=TRUE, none selected by default.
|
||||
expect_true(grepl(fixed = TRUE,
|
||||
|
||||
@@ -48,7 +48,7 @@ test_that("busyIndicatorOptions()", {
|
||||
|
||||
|
||||
test_that("Can provide svg file for busyIndicatorOptions(spinner_type)", {
|
||||
skip_on_os("windows")
|
||||
skip_if(.Platform$OS.type == "windows")
|
||||
|
||||
tmpsvg <- tempfile(fileext = ".svg")
|
||||
writeLines("<svg></svg>", tmpsvg)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
test_that("performance warning works", {
|
||||
pattern <- "consider using server-side selectize"
|
||||
|
||||
expect_no_warning(selectInput("x", "x", as.character(1:999)))
|
||||
expect_no_warning(selectInput("x", "x", as.character(1:999), selectize = TRUE))
|
||||
expect_no_warning(selectInput("x", "x", as.character(1:999), selectize = FALSE))
|
||||
expect_no_warning(selectizeInput("x", "x", as.character(1:999)))
|
||||
expect_warning(selectInput("x", "x", as.character(1:999)), NA)
|
||||
expect_warning(selectInput("x", "x", as.character(1:999), selectize = TRUE), NA)
|
||||
expect_warning(selectInput("x", "x", as.character(1:999), selectize = FALSE), NA)
|
||||
expect_warning(selectizeInput("x", "x", as.character(1:999)), NA)
|
||||
|
||||
expect_warning(selectInput("x", "x", as.character(1:1000)), pattern)
|
||||
expect_warning(selectInput("x", "x", as.character(1:1000), selectize = TRUE), pattern)
|
||||
@@ -17,9 +17,9 @@ test_that("performance warning works", {
|
||||
|
||||
session <- MockShinySession$new()
|
||||
|
||||
expect_no_warning(updateSelectInput(session, "x", choices = as.character(1:999)))
|
||||
expect_no_warning(updateSelectizeInput(session, "x", choices = as.character(1:999)))
|
||||
expect_no_warning(updateSelectizeInput(session, "x", choices = as.character(1:999), server = FALSE))
|
||||
expect_warning(updateSelectInput(session, "x", choices = as.character(1:999)), NA)
|
||||
expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:999)), NA)
|
||||
expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:999), server = FALSE), NA)
|
||||
|
||||
expect_warning(updateSelectInput(session, "x", choices = as.character(1:1000)), pattern)
|
||||
expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:1000)), pattern)
|
||||
@@ -28,9 +28,9 @@ test_that("performance warning works", {
|
||||
expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:2000)), pattern)
|
||||
expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:2000), server = FALSE), pattern)
|
||||
|
||||
expect_no_warning(updateSelectizeInput(session, "x", choices = as.character(1:999), server = TRUE))
|
||||
expect_no_warning(updateSelectizeInput(session, "x", choices = as.character(1:1000), server = TRUE))
|
||||
expect_no_warning(updateSelectizeInput(session, "x", choices = as.character(1:2000), server = TRUE))
|
||||
expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:999), server = TRUE), NA)
|
||||
expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:1000), server = TRUE), NA)
|
||||
expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:2000), server = TRUE), NA)
|
||||
})
|
||||
|
||||
|
||||
@@ -55,9 +55,9 @@ test_that("selectInput options are properly escaped", {
|
||||
))
|
||||
|
||||
si_str <- as.character(si)
|
||||
expect_match(si_str, "<option value=\""\">", fixed = TRUE, all = FALSE)
|
||||
expect_match(si_str, "<option value=\"'\">", fixed = TRUE, all = FALSE)
|
||||
expect_match(si_str, "<optgroup label=\""Separators"\">", fixed = TRUE, all = FALSE)
|
||||
expect_true(any(grepl("<option value=\""\">", si_str, fixed = TRUE)))
|
||||
expect_true(any(grepl("<option value=\"'\">", si_str, fixed = TRUE)))
|
||||
expect_true(any(grepl("<optgroup label=\""Separators"\">", si_str, fixed = TRUE)))
|
||||
})
|
||||
|
||||
|
||||
@@ -75,10 +75,10 @@ test_that("selectInputUI has a select at an expected location", {
|
||||
)
|
||||
# if this getter is changed, varSelectInput getter needs to be changed
|
||||
selectHtml <- selectInputVal$children[[2]]$children[[1]]
|
||||
expect_s3_class(selectHtml, "shiny.tag")
|
||||
expect_true(inherits(selectHtml, "shiny.tag"))
|
||||
expect_equal(selectHtml$name, "select")
|
||||
if (!is.null(selectHtml$attribs$class)) {
|
||||
expect_no_match(selectHtml$attribs$class, "symbol")
|
||||
expect_false(grepl(selectHtml$attribs$class, "symbol"))
|
||||
}
|
||||
|
||||
varSelectInputVal <- varSelectInput(
|
||||
@@ -91,9 +91,9 @@ test_that("selectInputUI has a select at an expected location", {
|
||||
)
|
||||
# if this getter is changed, varSelectInput getter needs to be changed
|
||||
varSelectHtml <- varSelectInputVal$children[[2]]$children[[1]]
|
||||
expect_s3_class(varSelectHtml, "shiny.tag")
|
||||
expect_true(inherits(varSelectHtml, "shiny.tag"))
|
||||
expect_equal(varSelectHtml$name, "select")
|
||||
expect_match(varSelectHtml$attribs$class, "symbol", fixed = TRUE)
|
||||
expect_true(grepl("symbol", varSelectHtml$attribs$class, fixed = TRUE))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ test_that("plotPNG()/startPNG() ignores NULL dimensions", {
|
||||
f <- plotPNG(function() plot(1), width = NULL, height = NULL)
|
||||
on.exit(unlink(f))
|
||||
bits <- readBin(f, "raw", file.info(f)$size)
|
||||
expect_gt(length(bits), 0)
|
||||
expect_true(length(bits) > 0)
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ test_that("ReactiveVal", {
|
||||
val <- reactiveVal()
|
||||
|
||||
isolate({
|
||||
expect_null(val())
|
||||
expect_true(is.null(val()))
|
||||
|
||||
# Set to a simple value
|
||||
val(1)
|
||||
@@ -99,12 +99,12 @@ test_that("ReactiveValues", {
|
||||
values <- reactiveValues(a=NULL, b=2)
|
||||
# a should exist and be NULL
|
||||
expect_setequal(isolate(names(values)), c("a", "b"))
|
||||
expect_null(isolate(values$a))
|
||||
expect_true(is.null(isolate(values$a)))
|
||||
|
||||
# Assigning NULL should keep object (not delete it), and set value to NULL
|
||||
values$b <- NULL
|
||||
expect_setequal(isolate(names(values)), c("a", "b"))
|
||||
expect_null(isolate(values$b))
|
||||
expect_true(is.null(isolate(values$b)))
|
||||
|
||||
|
||||
# Errors -----------------------------------------------------------------
|
||||
@@ -960,8 +960,8 @@ test_that("classes of reactive object", {
|
||||
})
|
||||
|
||||
test_that("{} and NULL also work in reactive()", {
|
||||
expect_no_error(reactive({}))
|
||||
expect_no_error(reactive(NULL))
|
||||
expect_error(reactive({}), NA)
|
||||
expect_error(reactive(NULL), NA)
|
||||
})
|
||||
|
||||
test_that("shiny.suppressMissingContextError option works", {
|
||||
|
||||
@@ -29,8 +29,8 @@ test_that("Render functions correctly handle quosures", {
|
||||
r1 <- inject(renderTable({ pressure[!!a, ] }, digits = 1))
|
||||
r2 <- renderTable({ eval_tidy(quo(pressure[!!a, ])) }, digits = 1)
|
||||
a <- 2
|
||||
expect_match(r1(), "0\\.0")
|
||||
expect_match(r2(), "20\\.0")
|
||||
expect_true(grepl("0\\.0", r1()))
|
||||
expect_true(grepl("20\\.0", r2()))
|
||||
})
|
||||
|
||||
test_that("functionLabel returns static value when the label can not be assigned to", {
|
||||
|
||||
@@ -227,7 +227,7 @@ test_that("observeEvent is not overly stripped (#4162)", {
|
||||
})
|
||||
)
|
||||
st_str <- capture.output(printStackTrace(caught), type = "message")
|
||||
expect_match(st_str, "observeEvent\\(1\\)", all = FALSE)
|
||||
expect_true(any(grepl("observeEvent\\(1\\)", st_str)))
|
||||
|
||||
# Now same thing, but deep stack trace version
|
||||
|
||||
@@ -257,6 +257,6 @@ test_that("observeEvent is not overly stripped (#4162)", {
|
||||
)
|
||||
st_str <- capture.output(printStackTrace(caught), type = "message")
|
||||
# cat(st_str, sep = "\n")
|
||||
expect_match(st_str, "A__", all = FALSE)
|
||||
expect_match(st_str, "B__", all = FALSE)
|
||||
expect_true(any(grepl("A__", st_str)))
|
||||
expect_true(any(grepl("B__", st_str)))
|
||||
})
|
||||
|
||||
@@ -115,5 +115,7 @@ test_that("tabItem titles can contain tag objects", {
|
||||
# "<a ....> <i>Hello</i> world"
|
||||
# As opposed to:
|
||||
# "<a ....><i>Hello</i> world
|
||||
expect_match(x$html, "<a [^>]+>\\s*<i>Hello</i>\\s+world")
|
||||
expect_true(
|
||||
grepl("<a [^>]+>\\s*<i>Hello</i>\\s+world", x$html)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -122,7 +122,7 @@ test_that("runTests runs as expected without rewiring", {
|
||||
appDir <- test_path(file.path("..", "test-helpers", "app1-standard"))
|
||||
df <- testthat::expect_output(
|
||||
print(runTests(appDir = appDir, assert = FALSE)),
|
||||
"Shiny App Test Results\\n\\v Success\\n - app1-standard/tests/runner1\\.R\\n - app1-standard/tests/runner2\\.R"
|
||||
"Shiny App Test Results\\n\\* Success\\n - app1-standard/tests/runner1\\.R\\n - app1-standard/tests/runner2\\.R"
|
||||
)
|
||||
|
||||
expect_equal(df, data.frame(
|
||||
|
||||
@@ -41,7 +41,7 @@ test_that("runTests works with a dir app that calls modules and uses testServer"
|
||||
app <- test_path("..", "test-modules", "12_counter")
|
||||
run <- testthat::expect_output(
|
||||
print(runTests(app)),
|
||||
"Shiny App Test Results\\n\\v Success\\n - 12_counter/tests/testthat\\.R"
|
||||
"Shiny App Test Results\\n\\* Success\\n - 12_counter/tests/testthat\\.R"
|
||||
)
|
||||
expect_true(all(run$pass))
|
||||
})
|
||||
@@ -50,7 +50,7 @@ test_that("runTests works with a dir app that calls modules that return reactive
|
||||
app <- test_path("..", "test-modules", "107_scatterplot")
|
||||
run <- testthat::expect_output(
|
||||
print(runTests(app)),
|
||||
"Shiny App Test Results\\n\\v Success\\n - 107_scatterplot/tests/testthat\\.R"
|
||||
"Shiny App Test Results\\n\\* Success\\n - 107_scatterplot/tests/testthat\\.R"
|
||||
)
|
||||
expect_true(all(run$pass))
|
||||
})
|
||||
|
||||
@@ -15,20 +15,22 @@ test_that("Radio buttons and checkboxes work with modules", {
|
||||
updateRadioButtons(sessA, "test1", label = "Label", choices = letters[1:5])
|
||||
resultA <- sessA$lastInputMessage
|
||||
|
||||
expect_equal(resultA$id, "test1")
|
||||
expect_equal(resultA$message$label, "Label")
|
||||
expect_equal(resultA$message$value, "a")
|
||||
expect_match(resultA$message$options, '"modA-test1"')
|
||||
expect_no_match(resultA$message$options, '"test1"')
|
||||
expect_equal("test1", resultA$id)
|
||||
expect_equal("Label", resultA$message$label)
|
||||
expect_equal("a", resultA$message$value)
|
||||
expect_true(grepl('"modA-test1"', resultA$message$options))
|
||||
expect_false(grepl('"test1"', resultA$message$options))
|
||||
|
||||
|
||||
sessB <- createModuleSession("modB")
|
||||
|
||||
updateCheckboxGroupInput(sessB, "test2", label = "Label", choices = LETTERS[1:5])
|
||||
resultB <- sessB$lastInputMessage
|
||||
|
||||
expect_equal(resultB$id, "test2")
|
||||
expect_equal(resultB$message$label, "Label")
|
||||
expect_equal("test2", resultB$id)
|
||||
expect_equal("Label", resultB$message$label)
|
||||
expect_null(resultB$message$value)
|
||||
expect_match(resultB$message$options, '"modB-test2"')
|
||||
expect_no_match(resultB$message$options, '"test2"')
|
||||
expect_true(grepl('"modB-test2"', resultB$message$options))
|
||||
expect_false(grepl('"test2"', resultB$message$options))
|
||||
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ test_that("Private randomness works at startup", {
|
||||
rm(".Random.seed", envir = .GlobalEnv)
|
||||
.globals$ownSeed <- NULL
|
||||
# Just make sure this doesn't blow up
|
||||
expect_no_error(createUniqueId(4))
|
||||
expect_error(createUniqueId(4), NA)
|
||||
})
|
||||
|
||||
test_that("Setting process-wide seed doesn't affect private randomness", {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"declaration": true,
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"target": "es2020",
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
|
||||
Reference in New Issue
Block a user