mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-10 23:48:01 -05:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3ad419ff1 | ||
|
|
13f505934a | ||
|
|
ab9a900fa2 |
@@ -128,6 +128,7 @@ Collate:
|
||||
'map.R'
|
||||
'utils.R'
|
||||
'bootstrap.R'
|
||||
'busy-indicators.R'
|
||||
'cache-utils.R'
|
||||
'deprecated.R'
|
||||
'devmode.R'
|
||||
|
||||
@@ -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)
|
||||
|
||||
186
R/busy-indicators.R
Normal file
186
R/busy-indicators.R
Normal 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"
|
||||
)
|
||||
}
|
||||
@@ -114,6 +114,7 @@ jqueryDependency <- function() {
|
||||
shinyDependencies <- function() {
|
||||
list(
|
||||
bslib::bs_dependency_defer(shinyDependencyCSS),
|
||||
busyIndicatorDependency(),
|
||||
htmlDependency(
|
||||
name = "shiny-javascript",
|
||||
version = get_package_version("shiny"),
|
||||
|
||||
2
inst/www/shared/busy-indicators/busy-indicators.css
Normal file
2
inst/www/shared/busy-indicators/busy-indicators.css
Normal file
File diff suppressed because one or more lines are too long
3
inst/www/shared/busy-indicators/busy-indicators.js
Normal file
3
inst/www/shared/busy-indicators/busy-indicators.js
Normal 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
|
||||
7
inst/www/shared/busy-indicators/busy-indicators.js.map
Normal file
7
inst/www/shared/busy-indicators/busy-indicators.js.map
Normal 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": []
|
||||
}
|
||||
@@ -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);
|
||||
@@ -18698,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) {
|
||||
@@ -18710,7 +18717,7 @@
|
||||
i5 = 0;
|
||||
case 5:
|
||||
if (!(i5 < bindings.length)) {
|
||||
_context.next = 34;
|
||||
_context.next = 35;
|
||||
break;
|
||||
}
|
||||
binding = bindings[i5].binding;
|
||||
@@ -18718,7 +18725,7 @@
|
||||
j3 = 0;
|
||||
case 9:
|
||||
if (!(j3 < matches.length)) {
|
||||
_context.next = 31;
|
||||
_context.next = 32;
|
||||
break;
|
||||
}
|
||||
_el2 = matches[j3];
|
||||
@@ -18727,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);
|
||||
@@ -18749,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");
|
||||
@@ -18757,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();
|
||||
}
|
||||
@@ -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",
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
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
2
inst/www/shared/shiny.min.js
vendored
2
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
23
man/pulseOptions.Rd
Normal file
23
man/pulseOptions.Rd
Normal 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
82
man/spinnerOptions.Rd
Normal 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
53
man/useBusyIndicators.Rd
Normal 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()}}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
3
srcts/extras/busy-indicators/ball.svg
Normal file
3
srcts/extras/busy-indicators/ball.svg
Normal 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 |
193
srcts/extras/busy-indicators/busy-indicators.scss
Normal file
193
srcts/extras/busy-indicators/busy-indicators.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
7
srcts/extras/busy-indicators/busy-indicators.ts
Normal file
7
srcts/extras/busy-indicators/busy-indicators.ts
Normal 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");
|
||||
});
|
||||
5
srcts/extras/busy-indicators/disc-spinner.svg
Normal file
5
srcts/extras/busy-indicators/disc-spinner.svg
Normal 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 |
5
srcts/extras/busy-indicators/dot-track-spinner.svg
Normal file
5
srcts/extras/busy-indicators/dot-track-spinner.svg
Normal 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 |
11
srcts/extras/busy-indicators/dots-spinner.svg
Normal file
11
srcts/extras/busy-indicators/dots-spinner.svg
Normal 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 |
5
srcts/extras/busy-indicators/tadpole-spinner.svg
Normal file
5
srcts/extras/busy-indicators/tadpole-spinner.svg
Normal 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 |
@@ -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 {
|
||||
|
||||
@@ -318,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");
|
||||
|
||||
@@ -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 = {};
|
||||
@@ -687,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]);
|
||||
}
|
||||
}
|
||||
@@ -1418,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",
|
||||
|
||||
@@ -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 };
|
||||
|
||||
2
srcts/types/src/shiny/shinyapp.d.ts
vendored
2
srcts/types/src/shiny/shinyapp.d.ts
vendored
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user