Compare commits

...

10 Commits

Author SHA1 Message Date
Carson
d3ad419ff1 Clear progress for only the bindings that we receive, not any binding that isn't in progress 2024-04-30 18:12:33 -05:00
Carson
13f505934a wip fix shiny.js showProgress logic 2024-04-30 17:59:09 -05:00
Carson
ab9a900fa2 Add useBusyIndicators() 2024-04-30 12:00:02 -05:00
Winston Chang
950c63049b Check that $socket exists before sending message (#4035)
* Check that socket exists before sending message

* `yarn build` (GitHub Actions)

---------

Co-authored-by: wch <wch@users.noreply.github.com>
2024-04-26 15:21:35 -05:00
Carson Sievert
3edf9bfad8 Fix opacity dimming on recalculating uiOutput() (#4028)
* Close #4027: Fix opacity dimming on recalculating uiOutput(). Also, only apply display:content when there are child elements

* Update inst/www/shared/shiny_scss/shiny.bootstrap5.scss

* Add news item
2024-04-10 13:16:41 -05:00
Carson Sievert
420a2c054c Start new version (#4023)
* Start new version

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2024-04-03 10:03:31 -05:00
Carson Sievert
5e566a057d Start v1.8.1.1 release candidate (#4020)
* Start v1.8.1.1 release candidate

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

* Remove alpha from npm version

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2024-04-03 09:26:01 -05:00
Carson Sievert
edd1db78e3 Warn instead of error when duplicate binding IDs are found in non-devmode (#4019)
* Close #4016. Warn instead of error when duplicate binding IDs are found in non-devmode

* Get rid of unreachable ShinyClientError()

* `yarn build` (GitHub Actions)

* Update srcts/src/shiny/bind.ts

Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>

* `yarn build` (GitHub Actions)

* Move logic to where error gets thrown not constructed

* `yarn build` (GitHub Actions)

* Update NEWS

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2024-03-29 13:51:46 -05:00
Joe Cheng
47526a769a ExtendedTask should not be cloneable (#4015) 2024-03-27 19:06:21 -05:00
Carson Sievert
0474eeeead Start new version (#4014)
* Start new version

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2024-03-27 10:10:45 -05:00
37 changed files with 732 additions and 99 deletions

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 1.8.1
Version: 1.8.1.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"),
@@ -128,6 +128,7 @@ Collate:
'map.R'
'utils.R'
'bootstrap.R'
'busy-indicators.R'
'cache-utils.R'
'deprecated.R'
'devmode.R'

View File

@@ -204,6 +204,7 @@ export(pre)
export(prependTab)
export(printError)
export(printStackTrace)
export(pulseOptions)
export(quoToFunction)
export(radioButtons)
export(reactive)
@@ -272,6 +273,7 @@ export(snapshotExclude)
export(snapshotPreprocessInput)
export(snapshotPreprocessOutput)
export(span)
export(spinnerOptions)
export(splitLayout)
export(stopApp)
export(strong)
@@ -317,6 +319,7 @@ export(updateTextInput)
export(updateVarSelectInput)
export(updateVarSelectizeInput)
export(urlModal)
export(useBusyIndicators)
export(validate)
export(validateCssUnit)
export(varSelectInput)

10
NEWS.md
View File

@@ -1,3 +1,13 @@
# shiny (development version)
## Bug fixes
* Fixed a recent issue with `uiOutput()` and `conditionalPanel()` not properly lower opacity when recalculation (in a Bootstrap 5 context). (#4027)
# shiny 1.8.1.1
* In v1.8.1, shiny.js starting throwing an error when input/output bindings have duplicate IDs. This error is now only thrown when `shiny::devmode(TRUE)` is enabled, so the issue is still made discoverable through the JS error console, but avoids unnecessarily breaking apps that happen to work with duplicate IDs. (#4019)
# shiny 1.8.1
## New features and improvements

186
R/busy-indicators.R Normal file
View File

@@ -0,0 +1,186 @@
#' Use and customize busy indicator types.
#'
#' To enable busy indicators, include the result of this function in the app's UI.
#'
#' When both `spinners` and `pulse` are set to `TRUE`, the pulse is disabled
#' when spinner(s) are active. When both `spinners` and `pulse` are set to
#' `FALSE`, no busy indication is shown (other than the gray-ing out of
#' recalculating outputs).
#'
#' @param spinners Overlay a spinner on each calculating/recalculating output.
#' @param pulse Show a pulsing banner at the top of the window when the server is busy.
#' @export
#' @seealso [spinnerOptions()] [pulseOptions()]
#' @examplesIf interactive()
#'
#' library(bslib)
#'
#' ui <- page_fillable(
#' useBusyIndicators(),
#' card(
#' card_header(
#' "A plot",
#' input_task_button("simulate", "Simulate"),
#' class = "d-flex justify-content-between align-items-center"
#' ),
#' plotOutput("p"),
#' )
#' )
#'
#' server <- function(input, output) {
#' output$p <- renderPlot({
#' input$simulate
#' Sys.sleep(4)
#' plot(x = rnorm(100), y = rnorm(100))
#' })
#' }
#'
#' shinyApp(ui, server)
useBusyIndicators <- function(spinners = TRUE, pulse = TRUE) {
attrs <- list("shinyBusySpinners" = spinners, "shinyBusyPulse" = pulse)
js <- Map(function(key, value) {
if (value) {
sprintf("document.documentElement.dataset.%s = 'true';", key)
} else {
sprintf("delete document.documentElement.dataset.%s;", key)
}
}, names(attrs), attrs)
js <- HTML(paste(js, collapse = "\n"))
# TODO: it'd be nice if htmltools had something like a page_attrs() that allowed us
# to do this without needing to inject JS into the head.
tags$script(js)
}
#' Customize spinning busy indicators.
#'
#' Include the result of this function in the app's UI to customize spinner
#' appearance.
#'
#' @details To effectively disable spinners, set the `size` to "0px".
#'
#' @param type The type of spinner to use. Builtin options include: tadpole,
#' disc, dots, dot-track, and bounce. A custom type may also provided, which
#' should be a valid value for the CSS
#' [mask-image](https://developer.mozilla.org/en-US/docs/Web/CSS/mask-image)
#' property.
#' @param color The color of the spinner. This can be any valid CSS color.
#' Defaults to the app's "primary" color (if Bootstrap is on the page) or
#' light-blue if not.
#' @param size The size of the spinner. This can be any valid CSS size.
#' @param easing The easing function to use for the spinner animation. This can
#' be any valid CSS [easing
#' function](https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function).
#' @param speed The amount of time for the spinner to complete a single
#' revolution. This can be any valid CSS time.
#' @param delay The amount of time to wait before showing the spinner. This can
#' be any valid CSS time and can useful for not showing the spinner
#' if the computation finishes quickly.
#' @param css_selector A CSS selector for scoping the spinner customization.
#' Defaults to the root element.
#'
#' @export
#' @seealso [useBusyIndicators()] [pulseOptions()]
#' @examplesIf interactive()
#'
#' library(bslib)
#'
#' ui <- page_fillable(
#' useBusyIndicators(),
#' spinnerOptions(color = "orange"),
#' card(
#' card_header(
#' "A plot",
#' input_task_button("simulate", "Simulate"),
#' class = "d-flex justify-content-between align-items-center"
#' ),
#' plotOutput("p"),
#' )
#' )
#'
#' server <- function(input, output) {
#' output$p <- renderPlot({
#' input$simulate
#' Sys.sleep(4)
#' plot(x = rnorm(100), y = rnorm(100))
#' })
#' }
#'
#' shinyApp(ui, server)
spinnerOptions <- function(
type = NULL,
...,
color = NULL,
size = NULL,
easing = NULL,
speed = NULL,
delay = NULL,
css_selector = ":root"
) {
# bounce requires a different animation than the others
if (isTRUE(type == "bounce")) {
animation <- "shiny-busy-spinner-bounce"
speed <- speed %||% "0.8s"
} else {
animation <- NULL
}
# Supported types have a CSS var already defined with their SVG data
if (isTRUE(type %in% c("tadpole", "disc", "dots", "dot-track", "bounce"))) {
type <- sprintf("var(--_shiny-spinner-type-%s)", type)
}
# Options are controlled via CSS variables.
css_vars <- paste0(c(
if (!is.null(type)) sprintf("--shiny-spinner-mask-img: %s", type),
if (!is.null(easing)) sprintf("--shiny-spinner-easing: %s", easing),
if (!is.null(animation)) sprintf("--shiny-spinner-animation: %s", animation),
if (!is.null(color)) sprintf("--shiny-spinner-color: %s", color),
if (!is.null(size)) sprintf("--shiny-spinner-size: %s", size),
if (!is.null(speed)) sprintf("--shiny-spinner-speed: %s", speed),
if (!is.null(delay)) sprintf("--shiny-spinner-delay: %s", delay)
), collapse = ";")
# The CSS cascade allows this to be called multiple times, and as long as the CSS
# selector is the same, the last call takes precedence. Also, css_selector allows
# for scoping of the spinner customization.
tags$style(HTML(paste0(css_selector, " {", css_vars, "}")))
}
#' Customize the pulsing busy indicator.
#'
#' Include the result of this function in the app's UI to customize the pulsing
#' banner.
#'
#' @param color The color of the pulsing banner. This can be any valid CSS
#' color. Defaults to the app's "primary" color (if Bootstrap is on the page)
#' or light-blue if not.
#' @param height The height of the pulsing banner. This can be any valid CSS
#' size. Defaults to "3.5px".
#' @export
#' @seealso [useBusyIndicators()] [spinnerOptions()]
pulseOptions <- function(color = NULL, height = NULL, speed = NULL) {
css_vars <- paste0(c(
if (!is.null(color)) sprintf("--shiny-pulse-color: %s", color),
if (!is.null(height)) sprintf("--shiny-pulse-height: %s", height),
if (!is.null(speed)) sprintf("--shiny-pulse-speed: %s", speed)
), collapse = ";")
tags$style(HTML(paste0(":root {", css_vars, "}")))
}
busyIndicatorDependency <- function() {
htmlDependency(
name = "shiny-busy-indicators",
version = get_package_version("shiny"),
src = "www/shared/busy-indicators",
package = "shiny",
stylesheet = "busy-indicators.css",
script = "busy-indicators.js"
)
}

View File

@@ -42,7 +42,7 @@
#' session to immediately unblock and carry on with other user interactions.
#'
#' @export
ExtendedTask <- R6Class("ExtendedTask", portable = TRUE,
ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
public = list(
#' @description
#' Creates a new `ExtendedTask` object. `ExtendedTask` should generally be

View File

@@ -114,6 +114,7 @@ jqueryDependency <- function() {
shinyDependencies <- function() {
list(
bslib::bs_dependency_defer(shinyDependencyCSS),
busyIndicatorDependency(),
htmlDependency(
name = "shiny-javascript",
version = get_package_version("shiny"),

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
/*! shiny 1.8.1.9000 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
"use strict";(function(){document.documentElement.classList.add("shiny-not-yet-idle");$(document).one("shiny:idle",function(){document.documentElement.classList.remove("shiny-not-yet-idle")});})();
//# sourceMappingURL=busy-indicators.js.map

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../../../srcts/extras/busy-indicators/busy-indicators.ts"],
"sourcesContent": ["// This of this like the .shiny-busy class that shiny.js puts on the root element,\n// except it's added before shiny.js is initialized, connected, etc.\n// TODO: maybe shiny.js should be doing something like this?\ndocument.documentElement.classList.add(\"shiny-not-yet-idle\");\n$(document).one(\"shiny:idle\", function () {\n document.documentElement.classList.remove(\"shiny-not-yet-idle\");\n});"],
"mappings": ";yBAGA,SAAS,gBAAgB,UAAU,IAAI,oBAAoB,EAC3D,EAAE,QAAQ,EAAE,IAAI,aAAc,UAAY,CACxC,SAAS,gBAAgB,UAAU,OAAO,oBAAoB,CAChE,CAAC",
"names": []
}

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,2 @@
/*! shiny 1.8.1 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.8.1.9000 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
#showcase-well{border-radius:0}.shiny-code{background-color:#fff;margin-bottom:0}.shiny-code code{font-family:Menlo,Consolas,Courier New,monospace}.shiny-code-container{margin-top:20px;clear:both}.shiny-code-container h3{display:inline;margin-right:15px}.showcase-header{font-size:16px;font-weight:400}.showcase-code-link{text-align:right;padding:15px}#showcase-app-container{vertical-align:top}#showcase-code-tabs{margin-right:15px}#showcase-code-tabs pre{border:none;line-height:1em}#showcase-code-tabs .nav,#showcase-code-tabs ul{margin-bottom:0}#showcase-code-tabs .tab-content{border-style:solid;border-color:#e5e5e5;border-width:0px 1px 1px 1px;overflow:auto;border-bottom-right-radius:4px;border-bottom-left-radius:4px}#showcase-app-code{width:100%}#showcase-code-position-toggle{float:right}#showcase-sxs-code{padding-top:20px;vertical-align:top}.showcase-code-license{display:block;text-align:right}#showcase-code-content pre{background-color:#fff}

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,3 @@
/*! shiny 1.8.1 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.8.1.9000 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
"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

View File

@@ -1,4 +1,4 @@
/*! shiny 1.8.1 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.8.1.9000 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
"use strict";
(function() {
var __create = Object.create;
@@ -12057,6 +12057,13 @@
value: function renderValue(el, data) {
(0, import_jquery23.default)(el).attr("href", data);
}
}, {
key: "showProgress",
value: function showProgress(el, show3) {
return;
el;
show3;
}
}]);
return DownloadLinkOutputBinding2;
}(OutputBinding);
@@ -18604,12 +18611,6 @@
};
}
function addBinding(id, bindingType) {
if (id === "") {
throw new ShinyClientError({
headline: "Empty ".concat(bindingType, " ID found"),
message: "Binding IDs must not be empty."
});
}
var existingBinding = bindings.get(id);
if (existingBinding) {
existingBinding.push(bindingType);
@@ -18704,7 +18705,7 @@
}
function _bindOutputs() {
_bindOutputs = _asyncToGenerator9(/* @__PURE__ */ _regeneratorRuntime9().mark(function _callee(_ref3) {
var sendOutputHiddenState, maybeAddThemeObserver, outputBindings, scope, $scope, bindings, i5, binding, matches, j3, _el2, id, $el, bindingAdapter, _args = arguments;
var sendOutputHiddenState, maybeAddThemeObserver, outputBindings, scope, $scope, bindings, i5, binding, matches, j3, _Shiny$shinyapp, _el2, id, $el, bindingAdapter, _args = arguments;
return _regeneratorRuntime9().wrap(function _callee$(_context) {
while (1)
switch (_context.prev = _context.next) {
@@ -18716,7 +18717,7 @@
i5 = 0;
case 5:
if (!(i5 < bindings.length)) {
_context.next = 34;
_context.next = 35;
break;
}
binding = bindings[i5].binding;
@@ -18724,7 +18725,7 @@
j3 = 0;
case 9:
if (!(j3 < matches.length)) {
_context.next = 31;
_context.next = 32;
break;
}
_el2 = matches[j3];
@@ -18733,20 +18734,20 @@
_context.next = 14;
break;
}
return _context.abrupt("continue", 28);
return _context.abrupt("continue", 29);
case 14:
if (import_jquery37.default.contains(document.documentElement, _el2)) {
_context.next = 16;
break;
}
return _context.abrupt("continue", 28);
return _context.abrupt("continue", 29);
case 16:
$el = (0, import_jquery37.default)(_el2);
if (!$el.hasClass("shiny-bound-output")) {
_context.next = 19;
break;
}
return _context.abrupt("continue", 28);
return _context.abrupt("continue", 29);
case 19:
maybeAddThemeObserver(_el2);
bindingAdapter = new OutputBindingAdapter(_el2, binding);
@@ -18755,6 +18756,10 @@
case 23:
$el.data("shiny-output-binding", bindingAdapter);
$el.addClass("shiny-bound-output");
if ((_Shiny$shinyapp = Shiny.shinyapp) !== null && _Shiny$shinyapp !== void 0 && _Shiny$shinyapp.isRecalculating(id)) {
if (binding.showProgress)
binding.showProgress(_el2, true);
}
if (!$el.attr("aria-live"))
$el.attr("aria-live", "polite");
bindingsRegistry.addBinding(id, "output");
@@ -18763,18 +18768,18 @@
binding: binding,
bindingType: "output"
});
case 28:
case 29:
j3++;
_context.next = 9;
break;
case 31:
case 32:
i5++;
_context.next = 5;
break;
case 34:
case 35:
setTimeout(sendImageSizeFns.regular, 0);
setTimeout(sendOutputHiddenState, 0);
case 36:
case 37:
case "end":
return _context.stop();
}
@@ -18848,13 +18853,19 @@
currentInputs = bindInputs(shinyCtx, scope);
bindingValidity = bindingsRegistry.checkValidity();
if (!(bindingValidity.status === "error")) {
_context2.next = 6;
_context2.next = 10;
break;
}
if (!Shiny.inDevMode()) {
_context2.next = 9;
break;
}
throw bindingValidity.error;
case 6:
case 9:
console.warn("[shiny] " + bindingValidity.error.message);
case 10:
return _context2.abrupt("return", currentInputs);
case 7:
case 11:
case "end":
return _context2.stop();
}
@@ -22844,6 +22855,7 @@
_defineProperty20(this, "$persistentProgress", /* @__PURE__ */ new Set());
_defineProperty20(this, "$values", {});
_defineProperty20(this, "$errors", {});
_defineProperty20(this, "$invalidated", /* @__PURE__ */ new Set());
_defineProperty20(this, "$conditionals", {});
_defineProperty20(this, "$pendingMessages", []);
_defineProperty20(this, "$activeRequests", {});
@@ -22871,6 +22883,7 @@
binding: function binding(message) {
var key = message.id;
var binding2 = this.$bindings[key];
this.$invalidated.add(key);
if (binding2) {
(0, import_jquery38.default)(binding2.el).trigger({
type: "shiny:outputinvalidated",
@@ -23217,10 +23230,10 @@
}, {
key: "$sendMsg",
value: function $sendMsg(msg) {
if (!this.$socket.readyState) {
this.$pendingMessages.push(msg);
} else {
if (this.$socket && this.$socket.readyState) {
this.$socket.send(msg);
} else {
this.$pendingMessages.push(msg);
}
}
}, {
@@ -23485,13 +23498,17 @@
}()
}, {
key: "_clearProgress",
value: function _clearProgress() {
for (var name in this.$bindings) {
if (hasOwnProperty(this.$bindings, name) && !this.$persistentProgress.has(name)) {
this.$bindings[name].showProgress(false);
}
value: function _clearProgress(name) {
if (hasOwnProperty(this.$bindings, name) && !this.$persistentProgress.has(name)) {
this.$bindings[name].showProgress(false);
}
}
}, {
key: "isRecalculating",
value: function isRecalculating(name) {
var hasResult = hasOwnProperty(this.$values, name) || hasOwnProperty(this.$errors, name);
return this.$invalidated.has(name) || !hasResult;
}
}, {
key: "_init",
value: function _init() {
@@ -23503,25 +23520,26 @@
while (1)
switch (_context8.prev = _context8.next) {
case 0:
_this3._clearProgress();
_context8.t0 = _regeneratorRuntime13().keys(message);
case 2:
case 1:
if ((_context8.t1 = _context8.t0()).done) {
_context8.next = 10;
_context8.next = 11;
break;
}
_key = _context8.t1.value;
if (!hasOwnProperty(message, _key)) {
_context8.next = 8;
_context8.next = 9;
break;
}
_this3._clearProgress(_key);
_this3.$persistentProgress.delete(_key);
_context8.next = 8;
_this3.$invalidated.delete(_key);
_context8.next = 9;
return _this3.receiveOutput(_key, message[_key]);
case 8:
_context8.next = 2;
case 9:
_context8.next = 1;
break;
case 10:
case 11:
case "end":
return _context8.stop();
}
@@ -25106,7 +25124,7 @@
var windowShiny2;
function setShiny(windowShiny_) {
windowShiny2 = windowShiny_;
windowShiny2.version = "1.8.1";
windowShiny2.version = "1.8.1.9000";
var _initInputBindings = initInputBindings(), inputBindings = _initInputBindings.inputBindings, fileInputBinding2 = _initInputBindings.fileInputBinding;
var _initOutputBindings = initOutputBindings(), outputBindings = _initOutputBindings.outputBindings;
setFileInputBinding(fileInputBinding2);

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

View File

@@ -38,8 +38,15 @@ $datepicker-disabled-color: $dropdown-link-disabled-color !default;
$shiny-file-active-shadow: $input-focus-box-shadow !default;
/* Treat conditional panels and uiOutput as "pass-through" containers */
.shiny-panel-conditional,
div:where(.shiny-html-output) {
display: contents;
/* uiOutput()/ conditionalPanel() are "pass-through" containers when they have children. */
&:has(> *) {
display: contents;
/* Pass along styles that no longer impact the pass-through container */
&.recalculating > * {
opacity: 0.3;
}
}
}

View File

@@ -52,7 +52,6 @@ session to immediately unblock and carry on with other user interactions.
\item \href{#method-ExtendedTask-invoke}{\code{ExtendedTask$invoke()}}
\item \href{#method-ExtendedTask-status}{\code{ExtendedTask$status()}}
\item \href{#method-ExtendedTask-result}{\code{ExtendedTask$result()}}
\item \href{#method-ExtendedTask-clone}{\code{ExtendedTask$clone()}}
}
}
\if{html}{\out{<hr>}}
@@ -165,22 +164,5 @@ invalidation will be ignored.
\if{html}{\out{<div class="r">}}\preformatted{ExtendedTask$result()}\if{html}{\out{</div>}}
}
}
\if{html}{\out{<hr>}}
\if{html}{\out{<a id="method-ExtendedTask-clone"></a>}}
\if{latex}{\out{\hypertarget{method-ExtendedTask-clone}{}}}
\subsection{Method \code{clone()}}{
The objects of this class are cloneable with this method.
\subsection{Usage}{
\if{html}{\out{<div class="r">}}\preformatted{ExtendedTask$clone(deep = FALSE)}\if{html}{\out{</div>}}
}
\subsection{Arguments}{
\if{html}{\out{<div class="arguments">}}
\describe{
\item{\code{deep}}{Whether to make a deep clone.}
}
\if{html}{\out{</div>}}
}
}
}

23
man/pulseOptions.Rd Normal file
View File

@@ -0,0 +1,23 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/busy-indicators.R
\name{pulseOptions}
\alias{pulseOptions}
\title{Customize the pulsing busy indicator.}
\usage{
pulseOptions(color = NULL, height = NULL, speed = NULL)
}
\arguments{
\item{color}{The color of the pulsing banner. This can be any valid CSS
color. Defaults to the app's "primary" color (if Bootstrap is on the page)
or light-blue if not.}
\item{height}{The height of the pulsing banner. This can be any valid CSS
size. Defaults to "3.5px".}
}
\description{
Include the result of this function in the app's UI to customize the pulsing
banner.
}
\seealso{
\code{\link[=useBusyIndicators]{useBusyIndicators()}} \code{\link[=spinnerOptions]{spinnerOptions()}}
}

82
man/spinnerOptions.Rd Normal file
View File

@@ -0,0 +1,82 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/busy-indicators.R
\name{spinnerOptions}
\alias{spinnerOptions}
\title{Customize spinning busy indicators.}
\usage{
spinnerOptions(
type = NULL,
...,
color = NULL,
size = NULL,
easing = NULL,
speed = NULL,
delay = NULL,
css_selector = ":root"
)
}
\arguments{
\item{type}{The type of spinner to use. Builtin options include: tadpole,
disc, dots, dot-track, and bounce. A custom type may also provided, which
should be a valid value for the CSS
\href{https://developer.mozilla.org/en-US/docs/Web/CSS/mask-image}{mask-image}
property.}
\item{color}{The color of the spinner. This can be any valid CSS color.
Defaults to the app's "primary" color (if Bootstrap is on the page) or
light-blue if not.}
\item{size}{The size of the spinner. This can be any valid CSS size.}
\item{easing}{The easing function to use for the spinner animation. This can
be any valid CSS \href{https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function}{easing function}.}
\item{speed}{The amount of time for the spinner to complete a single
revolution. This can be any valid CSS time.}
\item{delay}{The amount of time to wait before showing the spinner. This can
be any valid CSS time and can useful for not showing the spinner
if the computation finishes quickly.}
\item{css_selector}{A CSS selector for scoping the spinner customization.
Defaults to the root element.}
}
\description{
Include the result of this function in the app's UI to customize spinner
appearance.
}
\details{
To effectively disable spinners, set the \code{size} to "0px".
}
\examples{
\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
library(bslib)
ui <- page_fillable(
useBusyIndicators(),
spinnerOptions(color = "orange"),
card(
card_header(
"A plot",
input_task_button("simulate", "Simulate"),
class = "d-flex justify-content-between align-items-center"
),
plotOutput("p"),
)
)
server <- function(input, output) {
output$p <- renderPlot({
input$simulate
Sys.sleep(4)
plot(x = rnorm(100), y = rnorm(100))
})
}
shinyApp(ui, server)
\dontshow{\}) # examplesIf}
}
\seealso{
\code{\link[=useBusyIndicators]{useBusyIndicators()}} \code{\link[=pulseOptions]{pulseOptions()}}
}

53
man/useBusyIndicators.Rd Normal file
View File

@@ -0,0 +1,53 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/busy-indicators.R
\name{useBusyIndicators}
\alias{useBusyIndicators}
\title{Use and customize busy indicator types.}
\usage{
useBusyIndicators(spinners = TRUE, pulse = TRUE)
}
\arguments{
\item{spinners}{Overlay a spinner on each calculating/recalculating output.}
\item{pulse}{Show a pulsing banner at the top of the window when the server is busy.}
}
\description{
To enable busy indicators, include the result of this function in the app's UI.
}
\details{
When both \code{spinners} and \code{pulse} are set to \code{TRUE}, the pulse is disabled
when spinner(s) are active. When both \code{spinners} and \code{pulse} are set to
\code{FALSE}, no busy indication is shown (other than the gray-ing out of
recalculating outputs).
}
\examples{
\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
library(bslib)
ui <- page_fillable(
useBusyIndicators(),
card(
card_header(
"A plot",
input_task_button("simulate", "Simulate"),
class = "d-flex justify-content-between align-items-center"
),
plotOutput("p"),
)
)
server <- function(input, output) {
output$p <- renderPlot({
input$simulate
Sys.sleep(4)
plot(x = rnorm(100), y = rnorm(100))
})
}
shinyApp(ui, server)
\dontshow{\}) # examplesIf}
}
\seealso{
\code{\link[=spinnerOptions]{spinnerOptions()}} \code{\link[=pulseOptions]{pulseOptions()}}
}

View File

@@ -3,7 +3,7 @@
"homepage": "https://shiny.rstudio.com",
"repository": "github:rstudio/shiny",
"name": "@types/rstudio-shiny",
"version": "1.8.1",
"version": "1.8.1-alpha.9000",
"license": "GPL-3.0-only",
"main": "",
"browser": "",
@@ -53,7 +53,7 @@
"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",
"esbuild-sass-plugin": "^2.9.0",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest": "^27.0.4",

View File

@@ -17,6 +17,7 @@ build({
"srcts/extras/shiny-autoreload.ts",
"srcts/extras/shiny-showcase.ts",
"srcts/extras/shiny-testmode.ts",
"srcts/extras/busy-indicators/busy-indicators.ts",
],
outdir: outDir,
});
@@ -24,7 +25,7 @@ build({
// - Sass -----------------------------------------------------------
import autoprefixer from "autoprefixer";
import sassPlugin from "esbuild-plugin-sass";
import sassPlugin from "esbuild-sass-plugin";
// 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 postCssPlugin from "@deanc/esbuild-plugin-postcss";
@@ -53,3 +54,12 @@ build({
],
outfile: outDir + "shiny.min.css",
});
build({
...sassOpts,
entryPoints: ["srcts/extras/busy-indicators/busy-indicators.scss"],
outfile: outDir + "busy-indicators/busy-indicators.css",
plugins: [sassPlugin({ type: "css", sourceMap: false })],
loader: { ".svg": "dataurl" },
bundle: true,
metafile: true,
});

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<ellipse class="spinner_rXNP" cx="12" cy="5" rx="4" ry="4" />
</svg>

After

Width:  |  Height:  |  Size: 132 B

View File

@@ -0,0 +1,193 @@
/*
"Built-in" spinner types
It's important to list out all the types/svgs here so that esbuild can bundle them
with the CSS (see js/build.ts).
*/
:root {
--_shiny-spinner-type-tadpole: url(tadpole-spinner.svg);
--_shiny-spinner-type-disc: url(disc-spinner.svg);
--_shiny-spinner-type-dots: url(dots-spinner.svg);
--_shiny-spinner-type-dot-track: url(dot-track-spinner.svg);
--_shiny-spinner-type-bounce: url(ball.svg);
}
/* This data atttribute is set by ui.busy_indicators.use() */
[data-shiny-busy-spinners] {
/* This class gets set by busy_indicators.ts */
.recalculating {
position: relative;
overflow: visible; /* overflow:hidden can, in some cases, clip the spinner */
&::after {
position: absolute;
/* ui.busy_indicators.spinner_options() */
--_shiny-spinner-mask-img: var(--shiny-spinner-mask-img, var(--_shiny-spinner-type-tadpole));
--_shiny-spinner-color: var(--shiny-spinner-color, var(--bs-primary, #007bc2));
--_shiny-spinner-size: var(--shiny-spinner-size, 32px);
--_shiny-spinner-easing: var(--shiny-spinner-easing, ease-in-out);
--_shiny-spinner-speed: var(--shiny-spinner-speed, 2s);
--_shiny-spinner-delay: var(--shiny-spinner-delay, 0.5s);
--_shiny-spinner-animation: var(--shiny-spinner-animation, shiny-busy-spinner-spin);
/* Apply the spinner type as a mask */
mask-image: var(--_shiny-spinner-mask-img);
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
-webkit-mask-image: var(--_shiny-spinner-mask-img);
-webkit-mask-position: center;
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
/* Color, sizing, & positioning */
background: var(--_shiny-spinner-color);
width: var(--_shiny-spinner-size);
height: var(--_shiny-spinner-size);
inset: calc(50% - var(--_shiny-spinner-size) / 2);
/* Animation */
animation-name: var(--_shiny-spinner-animation);
animation-duration: var(--_shiny-spinner-speed);
animation-iteration-count: infinite;
animation-timing-function: var(--_shiny-spinner-easing);
animation-delay: var(--_shiny-spinner-delay);
content: "";
scale: 0;
}
/*
shiny.css puts `opacity: 0.3` on .recalculating, which unfortunately applies to
the spinner. Undo that, but still apply (smaller) opacity to immediate children
that aren't shiny-output-busy.
*/
opacity: 1;
> *:not(.recalculating) {
opacity: 0.2;
}
/*
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 shiny-output-busy children.
*/
&.shiny-html-output::after {
display: none;
}
}
}
/* Styles for the page-level pulse banner */
@mixin shiny-page-busy {
/* ui.busy_indicators.pulse_options() */
--_shiny-pulse-background: var(
--shiny-pulse-background,
linear-gradient(
120deg,
var(--bs-body-bg, #fff),
var(--bs-indigo, #4b00c1),
var(--bs-purple, #74149c),
var(--bs-pink, #bf007f),
var(--bs-body-bg, #fff)
)
);
--_shiny-pulse-height: var(--shiny-pulse-height, 5px);
--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.85s);
/* Color, sizing, & positioning */
position: fixed;
top: 0;
left: 0;
height: var(--_shiny-pulse-height);
background: var(--_shiny-pulse-background);
border-radius: 50%;
z-index: 9999;
/* Animation */
animation-name: busy-page-pulse;
animation-duration: var(--_shiny-pulse-speed);
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
content: ""; /* Used in a ::after context */
}
/*
In spinners+pulse mode (the recommended default), show a page-level banner if the
page is busy, but there are no shiny-output-busy elements.
*/
[data-shiny-busy-spinners][data-shiny-busy-pulse] {
&.shiny-busy:not(:has(.recalculating))::after {
@include shiny-page-busy;
}
&.shiny-not-yet-idle:not(:has(.recalculating))::after {
@include shiny-page-busy;
}
}
/* In pulse _only_ mode, show a page-level banner whenever shiny is busy. */
[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]) {
&.shiny-busy::after {
@include shiny-page-busy;
}
&.shiny-not-yet-idle::after {
@include shiny-page-busy;
}
}
/* Keyframes behind most spinner types */
@keyframes shiny-busy-spinner-spin {
0% {
scale: 1;
rotate: 0deg;
}
100% {
scale: 1;
rotate: 360deg;
}
}
/* For busy_indicators.spinner_options(type="bounce") */
@keyframes shiny-busy-spinner-bounce {
0% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
translate: 0 calc(var(--_shiny-spinner-size) * (5 / 24));
scale: 1 1;
}
46.875% {
translate: 0 calc(var(--_shiny-spinner-size) * (20 / 24));
scale: 1 1;
}
50% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
translate: 0 calc(var(--_shiny-spinner-size) * (20.5 / 24));
scale: 1.2 0.85;
}
53.125% {
scale: 1 1;
}
100% {
translate: 0 calc(var(--_shiny-spinner-size) * (5 / 24));
scale: 1 1;
}
}
/* Keyframes for the pulsing banner */
@keyframes busy-page-pulse {
0% {
left: -75%;
width: 75%;
}
50% {
left: 100%;
width: 75%;
}
/* Go back */
100% {
left: -75%;
width: 75%;
}
}

View File

@@ -0,0 +1,7 @@
// This of this like the .shiny-busy class that shiny.js puts on the root element,
// except it's added before shiny.js is initialized, connected, etc.
// TODO: maybe shiny.js should be doing something like this?
document.documentElement.classList.add("shiny-not-yet-idle");
$(document).one("shiny:idle", function () {
document.documentElement.classList.remove("shiny-not-yet-idle");
});

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M9 16.7833C13.2986 16.7833 17.3579 13.2986 17.3579 9C17.3579 4.70142 13.2986 1.21672 9 1.21672C4.70141 1.21672 1.21672 4.70142 1.21672 9C1.21672 13.2986 4.70141 16.7833 9 16.7833ZM9 17.8678C13.8976 17.8678 17.3579 13.8976 17.3579 9C17.3579 4.10245 13.8976 0.132202 9 0.132202C4.10245 0.132202 0.132198 4.10245 0.132198 9C0.132198 13.8976 4.10245 17.8678 9 17.8678Z"
fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 521 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25" />
<circle class="spinner_7WDj" cx="12" cy="2.5" r="1.5" />
</svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@@ -0,0 +1,11 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g class="spinner_Wezc">
<circle cx="12" cy="2.5" r="1.5" opacity=".14" />
<circle cx="16.75" cy="3.77" r="1.5" opacity=".29" />
<circle cx="20.23" cy="7.25" r="1.5" opacity=".43" />
<circle cx="21.50" cy="12.00" r="1.5" opacity=".57" />
<circle cx="20.23" cy="16.75" r="1.5" opacity=".71" />
<circle cx="16.75" cy="20.23" r="1.5" opacity=".86" />
<circle cx="12" cy="21.5" r="1.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 522 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path class="spinner_0XTQ"
d="M12,23a9.63,9.63,0,0,1-8-9.5,9.51,9.51,0,0,1,6.79-9.1A1.66,1.66,0,0,0,12,2.81h0a1.67,1.67,0,0,0-1.94-1.64A11,11,0,0,0,12,23Z" />
</svg>

After

Width:  |  Height:  |  Size: 234 B

View File

@@ -9,6 +9,13 @@ class DownloadLinkOutputBinding extends OutputBinding {
renderValue(el: HTMLElement, data: string): void {
$(el).attr("href", data);
}
// Progress shouldn't be shown on the download button
// (progress will be shown as a page level pulse instead)
showProgress(el: HTMLElement, show: boolean): void {
return;
el;
show;
}
}
interface FileDownloadEvent extends JQuery.Event {

View File

@@ -136,13 +136,6 @@ const bindingsRegistry = (() => {
* @param bindingType Binding type, either "input" or "output"
*/
function addBinding(id: string, bindingType: BindingTypes): void {
if (id === "") {
throw new ShinyClientError({
headline: `Empty ${bindingType} ID found`,
message: "Binding IDs must not be empty.",
});
}
const existingBinding = bindings.get(id);
if (existingBinding) {
@@ -325,6 +318,9 @@ async function bindOutputs(
await shinyAppBindOutput(id, bindingAdapter);
$el.data("shiny-output-binding", bindingAdapter);
$el.addClass("shiny-bound-output");
if (Shiny.shinyapp?.isRecalculating(id)) {
if (binding.showProgress) binding.showProgress(el, true);
}
if (!$el.attr("aria-live")) $el.attr("aria-live", "polite");
bindingsRegistry.addBinding(id, "output");
@@ -427,7 +423,12 @@ async function _bindAll(
// re-run, and then see the next collision, etc.
const bindingValidity = bindingsRegistry.checkValidity();
if (bindingValidity.status === "error") {
throw bindingValidity.error;
// Only throw if we're in dev mode. Otherwise, just log a warning.
if (Shiny.inDevMode()) {
throw bindingValidity.error;
} else {
console.warn("[shiny] " + bindingValidity.error.message);
}
}
return currentInputs;

View File

@@ -135,6 +135,7 @@ class ShinyApp {
// Cached values/errors
$values: { [key: string]: any } = {};
$errors: { [key: string]: ErrorsMessageValue } = {};
$invalidated: Set<string> = new Set();
// Conditional bindings (show/hide element based on expression)
$conditionals = {};
@@ -461,12 +462,10 @@ class ShinyApp {
}
$sendMsg(msg: MessageValue): void {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (!this.$socket!.readyState) {
this.$pendingMessages.push(msg);
if (this.$socket && this.$socket.readyState) {
this.$socket.send(msg);
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.$socket!.send(msg);
this.$pendingMessages.push(msg);
}
}
@@ -689,28 +688,32 @@ class ShinyApp {
}
}
private _clearProgress() {
for (const name in this.$bindings) {
if (
hasOwnProperty(this.$bindings, name) &&
!this.$persistentProgress.has(name)
) {
this.$bindings[name].showProgress(false);
}
private _clearProgress(name: string) {
if (
hasOwnProperty(this.$bindings, name) &&
!this.$persistentProgress.has(name)
) {
this.$bindings[name].showProgress(false);
}
}
isRecalculating(name: string): boolean {
const hasResult =
hasOwnProperty(this.$values, name) || hasOwnProperty(this.$errors, name);
return this.$invalidated.has(name) || !hasResult;
}
private _init() {
// Dev note:
// * Use arrow functions to allow the Types to propagate.
// * However, `_sendMessagesToHandlers()` will adjust the `this` context to the same _`this`_.
addMessageHandler("values", async (message: { [key: string]: any }) => {
this._clearProgress();
for (const key in message) {
if (hasOwnProperty(message, key)) {
this._clearProgress(key);
this.$persistentProgress.delete(key);
this.$invalidated.delete(key);
await this.receiveOutput(key, message[key]);
}
}
@@ -1420,6 +1423,8 @@ class ShinyApp {
const key = message.id;
const binding = this.$bindings[key];
this.$invalidated.add(key);
if (binding) {
$(binding.el).trigger({
type: "shiny:outputinvalidated",

View File

@@ -2,5 +2,6 @@ import { OutputBinding } from "./outputBinding";
declare class DownloadLinkOutputBinding extends OutputBinding {
find(scope: HTMLElement): JQuery<HTMLElement>;
renderValue(el: HTMLElement, data: string): void;
showProgress(el: HTMLElement, show: boolean): void;
}
export { DownloadLinkOutputBinding };

View File

@@ -37,6 +37,7 @@ declare class ShinyApp {
$errors: {
[key: string]: ErrorsMessageValue;
};
$invalidated: Set<string>;
$conditionals: {};
$pendingMessages: MessageValue[];
$activeRequests: {
@@ -76,6 +77,7 @@ declare class ShinyApp {
dispatchMessage(data: ArrayBufferLike | string): Promise<void>;
private _sendMessagesToHandlers;
private _clearProgress;
isRecalculating(name: string): boolean;
private _init;
progressHandlers: {
binding: (this: ShinyApp, message: {