diff --git a/NAMESPACE b/NAMESPACE index ba08cf5..f35a95e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,10 +4,12 @@ export(add_busy_bar) export(add_busy_gif) export(add_busy_spinner) export(add_loading_state) +export(block) export(busy_start_up) export(config_notify) export(config_report) export(hide_spinner) +export(html_dependency_block) export(html_dependency_busy) export(html_dependency_epic) export(html_dependency_freezeframe) @@ -46,6 +48,7 @@ export(show_spinner) export(spin_epic) export(spin_kit) export(stop_gif) +export(unblock) export(update_busy_bar) export(update_modal_progress) export(update_modal_spinner) diff --git a/NEWS.md b/NEWS.md index dd62f5a..9505874 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,8 @@ +# shinybusy 0.3.2 + +* New functions `block()` and `unblock()` to block/unblock elements (like outputs) during a lonng calculation. + + # shinybusy 0.3.1 * Use correct HTML dependencies. diff --git a/R/block.R b/R/block.R new file mode 100644 index 0000000..33d3513 --- /dev/null +++ b/R/block.R @@ -0,0 +1,62 @@ + +#' Block / unblock an UI element +#' +#' @param id Id of the element to block, for exemple an `outputId`. +#' @param text Text displayed below the blocking indicator. +#' @param type Type of blocking indicator. +#' @param ... Other configuration option, see [online documentation](https://notiflix.github.io/documentation#DocsBlock). +#' @param selector Custom CSS selector, if used `id` is ignored. +#' @param session Default Shiny session. +#' +#' @return No value. +#' @export +#' +#' @name block +#' +#' @example examples/block.R +block <- function(id, + text = "Loading", + type = c("standard", "hourglass", "circle", "arrows", "dots", "pulse"), + ..., + selector = NULL, + session = shiny::getDefaultReactiveDomain()) { + type <- match.arg(type) + insertUI( + selector = "html", + ui = tagList(html_dependency_block()), + immediate = TRUE, + where = "afterBegin", + session = session + ) + if (is.null(selector)) + selector <- paste0("#", session$ns(id)) + session$sendCustomMessage( + type = "shinybusy-block-output", + message = list( + selector = selector, + type = type, + text = text, + config = list(...) + ) + ) +} + +#' @param timeout Unblock after a delay. +#' @export +#' +#' @rdname block +unblock <- function(id, + selector = NULL, + timeout = 0, + session = shiny::getDefaultReactiveDomain()) { + if (is.null(selector)) + selector <- paste0("#", session$ns(id)) + session$sendCustomMessage( + type = "shinybusy-unblock-output", + message = list( + selector = selector, + timeout = timeout + ) + ) +} + diff --git a/R/dependencies.R b/R/dependencies.R index 12e3258..0300291 100644 --- a/R/dependencies.R +++ b/R/dependencies.R @@ -150,3 +150,16 @@ html_dependency_report <- function() { script = "report.js" ) } + +#' @export +#' @rdname html-dependencies +html_dependency_block <- function() { + htmlDependency( + name = "shinybusy-block", + version = packageVersion("shinybusy"), + src = list(file = "packer"), + package = "shinybusy", + script = "block.js", + head = "" + ) +} diff --git a/examples/block.R b/examples/block.R new file mode 100644 index 0000000..ff5aa5d --- /dev/null +++ b/examples/block.R @@ -0,0 +1,62 @@ +library(shinybusy) +library(shiny) + +ui <- fluidPage( + + tags$h3("Block Output"), + + + fluidRow( + column( + width = 6, + plotOutput(outputId = "plot1"), + actionButton("block_manually", "Block / unblock") + ), + column( + width = 6, + plotOutput(outputId = "plot2"), + actionButton("block_reac", "Block when calculating in reactive()") + ) + ) + +) + +server <- function(input, output, session) { + + output$plot1 <- renderPlot({ + barplot(table(floor(runif(100) * 6))) + }) + + observeEvent(input$block_manually, { + if (input$block_manually %% 2 == 1) { + block(id = "plot1", type = "pulse", svgColor = "#5ea4d8") + } else { + unblock(id = "plot1") + } + }) + + data_r <- reactive({ + input$block_reac + block( + id = "plot2", + type = "circle", + text = "Calculating, please wait...", + messageColor = "#FFF", + svgColor = "#FFF", + backgroundColor = "#5ea4d8" + ) + Sys.sleep(3) + data <- data.frame(x = rnorm(50), y = rnorm(50)) + unblock(id = "plot2", timeout = 300) + return(data) + }) + + output$plot2 <- renderPlot({ + plot(data_r()) + }) + +} + +if (interactive()) + shinyApp(ui, server) + diff --git a/inst/packer/block.js b/inst/packer/block.js new file mode 100644 index 0000000..8c7b71f --- /dev/null +++ b/inst/packer/block.js @@ -0,0 +1 @@ +(()=>{var e={666:function(e,t,i){var a,o;o=void 0!==i.g?i.g:"undefined"!=typeof window?window:this,a=function(){return function(e){"use strict";if(void 0===e&&void 0===e.document)return!1;var t,i="\n\nVisit documentation page to learn more: https://notiflix.github.io/documentation",a="Standard",o="Hourglass",n="Circle",r="Arrows",s="Dots",l="Pulse",c={ID:"NotiflixBlockWrap",querySelectorLimit:200,className:"notiflix-block",position:"absolute",zindex:1e3,backgroundColor:"rgba(255,255,255,0.9)",rtl:!1,fontFamily:"Quicksand",cssAnimation:!0,cssAnimationDuration:300,svgSize:"45px",svgColor:"#383838",messageFontSize:"14px",messageMaxLength:34,messageColor:"#383838"},m=function(e){return console.error("%c Notiflix Error ","padding:2px;border-radius:20px;color:#fff;background:#ff5549","\n"+e+i)},f=function(t){return t||(t="head"),null!==e.document[t]||(m('\nNotiflix needs to be appended to the "<'+t+'>" element, but you called it before the "<'+t+'>" element has been created.'),!1)},u=function(){var e={},t=!1,i=0;"[object Boolean]"===Object.prototype.toString.call(arguments[0])&&(t=arguments[0],i++);for(var a=function(i){for(var a in i)Object.prototype.hasOwnProperty.call(i,a)&&(t&&"[object Object]"===Object.prototype.toString.call(i[a])?e[a]=u(e[a],i[a]):e[a]=i[a])};i'},p=function(e,t){return e||(e="60px"),t||(t="#32c682"),''},b=function(e,t){return e||(e="60px"),t||(t="#32c682"),''},h=function(e,t){return e||(e="60px"),t||(t="#32c682"),''},k=function(e,t){return e||(e="60px"),t||(t="#32c682"),''},y=function(){return'[id^=NotiflixBlockWrap]{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;box-sizing:border-box;position:absolute;z-index:1000;font-family:"Quicksand",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:rgba(255,255,255,.9);text-align:center;animation-duration:.4s;width:100%;height:100%;left:0;top:0;border-radius:inherit;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}[id^=NotiflixBlockWrap] *{-webkit-box-sizing:border-box;box-sizing:border-box}[id^=NotiflixBlockWrap]>span[class*="-icon"]{display:block;width:45px;height:45px;position:relative;margin:0 auto}[id^=NotiflixBlockWrap]>span[class*="-icon"] svg{width:inherit;height:inherit}[id^=NotiflixBlockWrap]>span[class*="-message"]{position:relative;display:block;width:100%;margin:10px auto 0;padding:0 10px;font-family:inherit!important;font-weight:normal;font-size:14px;line-height:1.4}[id^=NotiflixBlockWrap].nx-with-animation{-webkit-animation:block-animation-fade .3s ease-in-out 0s normal;animation:block-animation-fade .3s ease-in-out 0s normal}@-webkit-keyframes block-animation-fade{0%{opacity:0}100%{opacity:1}}@keyframes block-animation-fade{0%{opacity:0}100%{opacity:1}}[id^=NotiflixBlockWrap].nx-with-animation.nx-remove{opacity:0;-webkit-animation:block-animation-fade-remove .3s ease-in-out 0s normal;animation:block-animation-fade-remove .3s ease-in-out 0s normal}@-webkit-keyframes block-animation-fade-remove{0%{opacity:1}100%{opacity:0}}@keyframes block-animation-fade-remove{0%{opacity:1}100%{opacity:0}}'},w=0,v=function(a,y,v,N,S,C){var B;if(Array.isArray(v)){if(v.length<1)return m("Array of HTMLElements should contains at least one HTMLElement."),!1;B=v}else if(Object.prototype.isPrototypeOf.call(NodeList.prototype,v)){if(v.length<1)return m("NodeListOf should contains at least one HTMLElement."),!1;B=Array.prototype.slice.call(v)}else{if("string"!=typeof v||(v||"").length<1||1===(v||"").length&&("#"===(v||"")[0]||"."===(v||"")[0]))return m("The selector parameter must be a string and matches a specified CSS selector(s)."),!1;var z=e.document.querySelectorAll(v);if(z.length<1)return m('You called the "Notiflix.Block..." function with "'+v+'" selector, but there is no such element(s) in the document.'),!1;B=z}t||x.Block.init({});var L=u(!0,t,{});if("object"==typeof N&&!Array.isArray(N)||"object"==typeof S&&!Array.isArray(S)){var T={};"object"==typeof N?T=N:"object"==typeof S&&(T=S),t=u(!0,t,T)}var M="";"string"==typeof N&&N.length>0&&(M=N),t.cssAnimation||(t.cssAnimationDuration=0);var A=c.className;"string"==typeof t.className&&(A=t.className.trim());var H,j,X="number"==typeof t.querySelectorLimit?t.querySelectorLimit:200,I=(B||[]).length>=X?X:B.length,D="nx-block-temporary-position";if(a)for(var E=["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr","html","head","title","script","style","iframe"],O=0;O-1)break;var W=F.querySelectorAll("[id^="+c.ID+"]");if(W.length<1){var q="";y&&(q=y===o?(H=t.svgSize,j=t.svgColor,H||(H="60px"),j||(j="#32c682"),''):y===n?p(t.svgSize,t.svgColor):y===r?b(t.svgSize,t.svgColor):y===s?h(t.svgSize,t.svgColor):y===l?k(t.svgSize,t.svgColor):g(t.svgSize,t.svgColor));var P=''+q+"",V="";M.length>0&&(M=M.length>t.messageMaxLength?d(M).substring(0,t.messageMaxLength)+"...":d(M),V=''+M+""),w++;var R=e.document.createElement("div");R.id=c.ID+"-"+w,R.className=A+(t.cssAnimation?" nx-with-animation":""),R.style.position=t.position,R.style.zIndex=t.zindex,R.style.background=t.backgroundColor,R.style.animationDuration=t.cssAnimationDuration+"ms",R.style.fontFamily='"'+t.fontFamily+'", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif',R.style.display="flex",R.style.flexWrap="wrap",R.style.flexDirection="column",R.style.alignItems="center",R.style.justifyContent="center",t.rtl&&(R.setAttribute("dir","rtl"),R.classList.add("nx-rtl-on")),R.innerHTML=P+V;var Q=e.getComputedStyle(F).getPropertyValue("position"),U="string"==typeof Q?Q.toLocaleLowerCase("en"):"relative",Y=Math.round(1.25*parseInt(t.svgSize))+40,G="";Y>(F.offsetHeight||0)&&(G="min-height:"+Y+"px;");var J;J=F.getAttribute("id")?"#"+F.getAttribute("id"):F.classList[0]?"."+F.classList[0]:(F.tagName||"").toLocaleLowerCase("en");var K="",Z=["absolute","relative","fixed","sticky"].indexOf(U)<=-1;if(Z||G.length>0){if(!f("head"))return!1;Z&&(K="position:relative!important;");var $='",_=e.document.createRange();_.selectNode(e.document.head);var ee=_.createContextualFragment($);e.document.head.appendChild(ee),F.classList.add(D)}F.appendChild(R)}}}else var te=function(i){var a=setTimeout((function(){null!==i.parentNode&&i.parentNode.removeChild(i);var t=i.getAttribute("id"),o=e.document.getElementById("Style-"+t);o&&null!==o.parentNode&&o.parentNode.removeChild(o),clearTimeout(a)}),t.cssAnimationDuration)},ie=function(e){if(e&&e.length>0)for(var t=0;t" or "NodeListOf" does not have a "Block" element to remove.')},ae=function(e){var i=setTimeout((function(){e.classList.remove(D),clearTimeout(i)}),t.cssAnimationDuration+300)},oe=setTimeout((function(){for(var e=0;e{"use strict";Shiny;var e=i(666);Shiny.addCustomMessageHandler("shinybusy-block-output",(function(t){t.hasOwnProperty("text")?e.Block[t.type](t.selector,t.text,t.config):e.Block[t.type](t.selector,t.config)})),Shiny.addCustomMessageHandler("shinybusy-unblock-output",(function(t){e.Block.remove(t.selector,t.timeout)}))})()})(); \ No newline at end of file diff --git a/man/block.Rd b/man/block.Rd new file mode 100644 index 0000000..dfcd306 --- /dev/null +++ b/man/block.Rd @@ -0,0 +1,108 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/block.R +\name{block} +\alias{block} +\alias{unblock} +\title{Block / unblock an UI element} +\usage{ +block( + id, + text = "Loading", + type = c("standard", "hourglass", "circle", "arrows", "dots", "pulse"), + ..., + selector = NULL, + session = shiny::getDefaultReactiveDomain() +) + +unblock( + id, + selector = NULL, + timeout = 0, + session = shiny::getDefaultReactiveDomain() +) +} +\arguments{ +\item{id}{Id of the element to block, for exemple an \code{outputId}.} + +\item{text}{Text displayed below the blocking indicator.} + +\item{type}{Type of blocking indicator.} + +\item{...}{Other configuration option, see \href{https://notiflix.github.io/documentation#DocsBlock}{online documentation}.} + +\item{selector}{Custom CSS selector, if used \code{id} is ignored.} + +\item{session}{Default Shiny session.} + +\item{timeout}{Unblock after a delay.} +} +\value{ +No value. +} +\description{ +Block / unblock an UI element +} +\examples{ +library(shinybusy) +library(shiny) + +ui <- fluidPage( + + tags$h3("Block Output"), + + + fluidRow( + column( + width = 6, + plotOutput(outputId = "plot1"), + actionButton("block_manually", "Block / unblock") + ), + column( + width = 6, + plotOutput(outputId = "plot2"), + actionButton("block_reac", "Block when calculating in reactive()") + ) + ) + +) + +server <- function(input, output, session) { + + output$plot1 <- renderPlot({ + barplot(table(floor(runif(100) * 6))) + }) + + observeEvent(input$block_manually, { + if (input$block_manually \%\% 2 == 1) { + block(id = "plot1", type = "pulse", svgColor = "#5ea4d8") + } else { + unblock(id = "plot1") + } + }) + + data_r <- reactive({ + input$block_reac + block( + id = "plot2", + type = "circle", + text = "Calculating, please wait...", + messageColor = "#FFF", + svgColor = "#FFF", + backgroundColor = "#5ea4d8" + ) + Sys.sleep(3) + data <- data.frame(x = rnorm(50), y = rnorm(50)) + unblock(id = "plot2", timeout = 300) + return(data) + }) + + output$plot2 <- renderPlot({ + plot(data_r()) + }) + +} + +if (interactive()) + shinyApp(ui, server) + +} diff --git a/man/html-dependencies.Rd b/man/html-dependencies.Rd index ea49871..175a4fb 100644 --- a/man/html-dependencies.Rd +++ b/man/html-dependencies.Rd @@ -13,6 +13,7 @@ \alias{html_dependency_busy} \alias{html_dependency_notify} \alias{html_dependency_report} +\alias{html_dependency_block} \title{HTML dependencies used by shinybusy} \usage{ html_dependency_spinkit() @@ -36,6 +37,8 @@ html_dependency_busy() html_dependency_notify() html_dependency_report() + +html_dependency_block() } \value{ an \code{\link[htmltools:htmlDependency]{htmltools::htmlDependency()}}. diff --git a/srcjs/config/entry_points.json b/srcjs/config/entry_points.json index e801c2b..a1d301e 100644 --- a/srcjs/config/entry_points.json +++ b/srcjs/config/entry_points.json @@ -3,5 +3,6 @@ "loading": "./srcjs/exts/loading.js", "busy": "./srcjs/exts/busy.js", "notify": "./srcjs/exts/notify.js", - "report": "./srcjs/exts/report.js" + "report": "./srcjs/exts/report.js", + "block": "./srcjs/exts/block.js" } diff --git a/srcjs/exts/block.js b/srcjs/exts/block.js new file mode 100644 index 0000000..e327fe0 --- /dev/null +++ b/srcjs/exts/block.js @@ -0,0 +1,13 @@ +import "shiny"; +import { Block } from 'notiflix/build/notiflix-block-aio'; + +Shiny.addCustomMessageHandler("shinybusy-block-output", function(data) { + if (data.hasOwnProperty("text")) { + Block[data.type](data.selector, data.text, data.config); + } else { + Block[data.type](data.selector,data.config); + } +}); +Shiny.addCustomMessageHandler("shinybusy-unblock-output", function(data) { + Block.remove(data.selector, data.timeout); +});