当Shiny应用程序正在忙碌时,如何防止用户执行任何操作。

4
我有一个复杂的闪亮应用程序,具有许多输入、叶片地图等等…… 我的问题是当应用程序正在进行一些计算时,用户会继续点击应用程序上的任何位置,有时应用程序会崩溃。
我希望防止用户在应用程序繁忙时进行任何点击。重要的是,旋转器保持旋转状态,而不是像“waiter”包中的整个页面等待器。也许有一种可能性将旋转器和等待器结合起来?但我还没有找出如何做到。
我在这里有一个小的reprex: 当我点击“繁忙的应用程序”按钮时,会出现一个5秒的旋转器,让用户等待。但在此期间,用户仍然可以点击“增量”按钮。在旋转器结束时,输出增加了所进行的点击次数。 我想一次性封锁整个应用程序,而不仅仅是在应用程序繁忙时在按钮上放置一个“禁用”(因为我的应用程序中有很多输入,这需要太多修改)。
library(shiny)

ui <- fluidPage(

  # spinner css
  tags$head(
    tags$style(HTML("
  #loadmessage {
  position:fixed; z-index:8; top:50%; left:50%; padding:10px;
  text-align:center; font-weight:bold; color:#000000; background-color:#CCFF66;
  }
  
  .loader {
  position:fixed; z-index:8; border:16px solid #999999;
  border-top: 16px solid #8B0000; border-radius: 50%;
  width: 120px; height: 120px; top:45%; left:45%;
  animation: spin 2s linear infinite;
  }

  @keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
  }"))
  ),
  
  # display load spinner when shiny is busy
  conditionalPanel(
    condition = "$(\'html\').hasClass(\'shiny-busy\')",
    tags$div(class = "loader")
  ),
  actionButton(
    inputId = "increment",
    label = "Increment"
  ),
  textOutput("result"),
  actionButton(
    inputId = "busy",
    label = "Busy app"
  )
)

server <- function(input, output, session) {
  
  rv <- reactiveValues(counter = 0)

  #increment counter
  observeEvent(input$increment,{
    rv$counter = rv$counter + 1
  })
  
  #display incremented counter
  output$result <- renderText({
    rv$counter
  })
  
  observeEvent(input$busy, {
    Sys.sleep(5)
    # during this time, the user should not be able to do anything on the app
  })
}

shinyApp(ui = ui, server = server)

也许这个链接可以帮助。 - maydin
我看到了,但我正在尝试寻找更全局的东西,不仅仅是针对输入。我有地图、多边形、标记等也可以点击。我在想当shiny忙碌时,也许可以通过一些JS禁用左键单击。 - gdevaux
使用弹出窗口? - denis
太过于侵入性了,它会每秒弹出一次。当用户在地图上移动时,地图正在加载数据,因此弹出窗口会每秒弹出0.1秒。 - gdevaux
3个回答

4

经过分析您的答案并思考更多,我找到了最简单的解决方案。

在“正在繁忙”事件中,我在条件面板中显示一个div,该面板占据整个屏幕且处于第一平面,因此它可以防止任何点击输入/输出。当应用程序不再繁忙时,面板消失。面板是透明的,所以用户看不到它。

此外,它使我能够禁用所有输入和输出,而不依赖于计时器,仅取决于应用程序是否繁忙。

library(shiny)

ui <- fluidPage(

  # spinner css
  tags$head(
    tags$style(HTML("
  #loadmessage {
  position:fixed; z-index:8; top:50%; left:50%; padding:10px;
  text-align:center; font-weight:bold; color:#000000; background-color:#CCFF66;
  }
  
  .loader {
  position:fixed; z-index:8; border:16px solid #999999;
  border-top: 16px solid #8B0000; border-radius: 50%;
  width: 120px; height: 120px; top:45%; left:45%;
  animation: spin 2s linear infinite;
  }

  .prevent_click{
  position:fixed; 
  z-index:9;
  width:100%;
  height:100vh;
  background-color: transpare'nt;   
  }

  @keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
  }"))
  ),
  
  # display load spinner when shiny is busy
  conditionalPanel(
    condition = "$(\'html\').hasClass(\'shiny-busy\')",
    tags$div(class = "loader"),
    tags$div(class = "prevent_click")
  ),
  actionButton(
    inputId = "increment",
    label = "Increment"
  ),
  textOutput("result"),
  actionButton(
    inputId = "busy",
    label = "Busy app"
  )
)

server <- function(input, output, session) {
  
  rv <- reactiveValues(counter = 0)

  #increment counter
  observeEvent(input$increment,{
    rv$counter = rv$counter + 1
  })
  
  #display incremented counter
  output$result <- renderText({
    rv$counter
  })
  
  observeEvent(input$busy, {
    Sys.sleep(5)
    # during this time, the user should not be able to do anything on the app
  })
}

shinyApp(ui = ui, server = server)

2
我们可以尝试使用shinyjs库结合walk函数来禁用所有输入。
  observeEvent(input$busy, {
    names(input) %>% walk(disable)
    Sys.sleep(3) 
    # during this time, the user should not be able to do anything on the app
    names(input) %>% walk(enable)
  })
}

app:

library(shiny)
library(shinyjs)
library(tidyverse)

ui <- fluidPage(
  useShinyjs(),
  # spinner css
  tags$head(
    tags$style(HTML("
  #loadmessage {
  position:fixed; z-index:8; top:50%; left:50%; padding:10px;
  text-align:center; font-weight:bold; color:#000000; background-color:#CCFF66;
  }
  
  .loader {
  position:fixed; z-index:8; border:16px solid #999999;
  border-top: 16px solid #8B0000; border-radius: 50%;
  width: 120px; height: 120px; top:45%; left:45%;
  animation: spin 2s linear infinite;
  }

  @keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
  }"))
  ),
  
  # display load spinner when shiny is busy
  conditionalPanel(
    condition = "$(\'html\').hasClass(\'shiny-busy\')",
    tags$div(class = "loader")
  ),
  actionButton(
    inputId = "increment",
    label = "Increment"
  ),
  textOutput("result"),
  actionButton(
    inputId = "busy",
    label = "Busy app"
  )
)

server <- function(input, output, session) {
  
  rv <- reactiveValues(counter = 0)
  
  #increment counter
  observeEvent(input$increment,{
    rv$counter = rv$counter + 1
  })
  
  #display incremented counter
  output$result <- renderText({
    rv$counter
  })
  
  observeEvent(input$busy, {
    names(input) %>% walk(disable)
    Sys.sleep(3) 
    # during this time, the user should not be able to do anything on the app
    names(input) %>% walk(enable)
  })
}

shinyApp(ui = ui, server = server)

1
谢谢你的回答。我不知道如何使它独立于计时器并且只在应用程序忙碌时禁用元素。但是,我找到了一个简单的CSS解决方案,适合我所需的一切。我刚刚发布了答案。 - gdevaux

2

我也会使用shinyjs,但不需要遍历所有输入项,而是使用一些CSS并更改所有输入项和按钮的pointer-events。您可以根据需要调整CSS选择器。

  observeEvent(input$busy, {
    shinyjs::runjs('$("input, button").css("pointer-events", "none")')
    Sys.sleep(5)
    # during this time, the user should not be able to do anything on the app
    shinyjs::runjs('$("input, button").css("pointer-events", "unset")')
  })

谢谢您的回答。我不知道如何使其独立于计时器并仅在应用程序忙碌时禁用元素。但是,我找到了一个简单的CSS解决方案,适合我所需的一切。我刚刚发布了答案。 - gdevaux

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接