1 2knit_params_get <- function(input_lines, params) { 3 4 # read the default parameters and extract them into a named list 5 knit_params <- knitr::knit_params(input_lines) 6 if (packageVersion('yaml') < '2.1.14') knit_params <- mark_utf8(knit_params) 7 default_params <- list() 8 for (param in knit_params) { 9 default_params[param$name] <- list(param$value) 10 } 11 12 # validate params passed to render 13 if (!is.null(params)) { 14 15 if (identical(params, "ask")) { 16 params <- knit_params_ask( 17 input_lines = input_lines, shiny_args = list(launch.browser = TRUE) 18 ) 19 if (is.null(params)) { 20 stop("render parameter configuration canceled") 21 } 22 } 23 24 # verify they are a list 25 if (!is.list(params) || (length(names(params)) != length(params))) { 26 stop("render params argument must be a named list") 27 } 28 29 # verify that all parameters passed are also in the yaml 30 invalid_params <- setdiff(names(params), names(default_params)) 31 if (length(invalid_params) > 0) { 32 stop("render params not declared in YAML: ", 33 paste(invalid_params, collapse = ", ")) 34 } 35 } 36 37 # merge explicitly provided params with defaults 38 merge_lists(default_params, params, recursive = FALSE) 39} 40 41params_label <- function(inputControlFn, param) { 42 label <- ifelse(is.null(param$label), param$name, param$label) 43 if (identical(inputControlFn, shiny::fileInput)) { 44 if (is.character(param$value)) { 45 label <- paste0(label, " (default: ", param$value, ")") 46 } 47 } 48 label 49} 50 51params_value_to_ui <- function(inputControlFn, value, showDefault) { 52 if (is.null(showDefault)) { 53 showDefault <- TRUE 54 } 55 56 isNumericInput <- identical(inputControlFn, shiny::numericInput) || 57 identical(inputControlFn, shiny::sliderInput) 58 59 if (identical(inputControlFn, shiny::fileInput)) { 60 NULL 61 } else if (identical(inputControlFn, shiny::textInput)) { 62 ## TODO: if long input, maybe truncate textInput values for display 63 64 if (showDefault) { 65 classes <- class(value) 66 if ("POSIXct" %in% classes) { 67 as.character(value) 68 } else { 69 value 70 } 71 } else { 72 NULL 73 } 74 } else if (is.null(value)) { 75 # The numerics can't deal with a NULL value, but everything else is fine. 76 if (isNumericInput) { 77 0 78 } else { 79 value 80 } 81 } else { 82 if (showDefault) { 83 ## A type/control that doesn't need special handling; just emit the value. 84 value 85 } else { 86 if (isNumericInput) { 87 0 88 } else if (identical(inputControlFn, shiny::dateInput)) { 89 # Use NA to clear date inputs: 90 # https://github.com/rstudio/shiny/pull/1299 91 NA 92 } else if (identical(inputControlFn, shiny::radioButtons)) { 93 # As suggested in ?radioButtons 94 character(0) 95 } else { 96 NULL 97 } 98 } 99 } 100} 101 102params_value_from_ui <- function(inputControlFn, value, uivalue) { 103 if (identical(inputControlFn, shiny::fileInput)) { 104 backup_file_input(uivalue$datapath) 105 } else if (identical(inputControlFn, shiny::textInput)) { 106 classes <- class(value) 107 if ("POSIXct" %in% classes) { 108 if (identical(uivalue, "")) { 109 # show_default: false produces this situation 110 # Empty POSIXct 111 Sys.time()[-1] 112 } else { 113 as.POSIXct(uivalue) 114 } 115 } else { 116 uivalue 117 } 118 } else { 119 ## A type/control that doesn't need special handling; just emit the value. 120 uivalue 121 } 122} 123 124# Uploaded files will be deleted when the shiny UI is closed, so we need to back 125# them up to new temp files: https://github.com/rstudio/rmarkdown/issues/919 126backup_file_input <- function(files) { 127 files2 <- files 128 for (i in seq_along(files)) { 129 dir.create(d <- tempfile()) 130 files2[i] <- file.path(d, basename(files[i])) 131 } 132 file.copy(files, files2) 133 files2 134} 135 136params_get_input <- function(param) { 137 # Maps between value types and input: XXX 138 default_inputs <- list( 139 logical = "checkbox", 140 Date = "date", 141 ## BUG: shiny does not support datetime selectors 142 ## https://github.com/rstudio/shiny/issues/897 143 ## we ask for string input for now. 144 POSIXct = "datetime", 145 character = "text" 146 ) 147 default_inputs$integer <- default_inputs$numeric <- { 148 ## If min/max are specified, use a slider. 149 if (is.null(param$min) || is.null(param$max)) { 150 "numeric" 151 } else { 152 "slider" 153 } 154 } 155 156 input <- param$input 157 if (is.null(input)) { 158 if (!is.null(param$choices)) { 159 ## radio buttons for a small number of choices, select otherwise. 160 if (length(param$choices) <= 4) { 161 input <- "radio" 162 } else { 163 input <- "select" 164 } 165 } else { 166 ## Not choices. Look at the value type to find what input control we 167 ## should use. 168 169 ## A value might have multiple classes. Try: class(Sys.time()) 170 ## Try to find first class listed with a named control. 171 for (c in class(param$value)) { 172 default_input <- default_inputs[[c]] 173 if (!is.null(default_input)) { 174 input <- default_input 175 break 176 } 177 } 178 } 179 } 180 input 181} 182 183params_get_control <- function(param) { 184 input <- params_get_input(param) 185 if (is.null(input)) { 186 return(NULL) 187 } 188 189 # Maps between input: XXX and the various Shiny input controls 190 input_controls <- list( 191 checkbox = shiny::checkboxInput, 192 numeric = shiny::numericInput, 193 slider = shiny::sliderInput, 194 date = shiny::dateInput, 195 datetime = shiny::textInput, # placeholder for future datetime picker 196 text = shiny::textInput, 197 file = shiny::fileInput, 198 radio = shiny::radioButtons, 199 select = shiny::selectInput 200 ) 201 control <- input_controls[[input]] 202 if (is.null(control)) { 203 stop(paste("could not determine what control to use for parameter", param$name, "with input:", input)) 204 } 205 control 206} 207 208# Returns true if the parameter can be configurable with Shiny UI elements. 209params_configurable <- function(param) { 210 if (is.null(params_get_control(param))) { 211 return(FALSE) # no Shiny control 212 } 213 multiple_ok <- (!is.null(param$multiple) && param$multiple) 214 if (multiple_ok) { 215 return(TRUE) 216 } 217 return(length(param$value) <= 1) # multiple values only when multi-input controls 218} 219 220# Returns a new empty named list. 221params_namedList <- function() { 222 empty <- list() 223 names(empty) <- character() 224 empty 225} 226 227#' Run a shiny application asking for parameter configuration for the given document. 228#' 229#' @param file Path to the R Markdown document with configurable parameters. 230#' @param input_lines Content of the R Markdown document. If \code{NULL}, the contents of \code{file} will be read. 231#' @param params A named list of optional parameter overrides used in place of the document defaults. 232#' @param shiny_args Additional arguments to \code{\link[shiny:runApp]{runApp}}. 233#' @param save_caption Caption to use use for button that saves/confirms parameters. 234#' @param encoding The encoding of the input file; see \code{\link{file}}. 235#' 236#' @return named list with overridden parameter names and value. 237#' 238#' @export 239knit_params_ask <- function(file = NULL, 240 input_lines = NULL, 241 params = NULL, 242 shiny_args = NULL, 243 save_caption = "Save", 244 encoding = getOption("encoding")) { 245 246 if (is.null(input_lines)) { 247 if (is.null(file)) { 248 stop("knit_params_ask must have a non-NULL file or input_lines parameter") 249 } 250 input_lines <- read_lines_utf8(file, encoding) 251 } 252 253 knit_params <- knitr::knit_params(input_lines) 254 if (packageVersion('yaml') < '2.1.14') knit_params <- mark_utf8(knit_params) 255 256 ## Input validation on params (checks shared with render) 257 if (!is.null(params)) { 258 ## Must be a named list 259 if (!is.list(params) || (length(names(params)) != length(params))) { 260 stop("knit_params_ask params argument must be a named list") 261 } 262 ## We do not validate names(params) because the document may have changed 263 ## but we're loading parameters that were configured with an older 264 ## version. 265 } 266 267 ## If we happen to not have any knit_params, just return an empty named list 268 ## and don't fire up the Shiny app. 269 if (length(knit_params) == 0) { 270 return(params_namedList()) 271 } 272 273 configurable <- Filter(params_configurable, knit_params) 274 unconfigurable <- Filter(Negate(params_configurable), knit_params) 275 276 ## This set of published values is the raw set that came from the user. 277 ## It does not include those values that cannot be configured or are 278 ## left to use the default. 279 values <- params_namedList() 280 281 server <- function(input, output, session) { 282 param.ui <- function(param) { 283 inputControlFn <- params_get_control(param) 284 inputControlFnFormals <- names(formals(inputControlFn)) 285 286 inputId <- param$name 287 label <- params_label(inputControlFn, param) 288 289 arguments = list( 290 inputId = inputId, 291 label = label 292 ) 293 294 # We MUST process the "value" name even if it is not present (due to 295 # NULL values). 296 attrib_names <- unique(c(names(param), "value")) 297 lapply(attrib_names, function(name) { 298 if (name %in% c("name", "input", "expr")) { 299 } else if (name == "label") { 300 arguments$label <<- label 301 } else if (name == "value") { 302 303 ## The current value for this parameter is either `params` when 304 ## overridden by our caller or `param$value` otherwise. 305 current_value <- param$value 306 if (!is.null(params)) { 307 override <- params[[param$name]] 308 if (!is.null(override)) { 309 current_value <- override 310 } 311 } 312 # Now, transform into something that the input control can handle. 313 current_value <- params_value_to_ui(inputControlFn, current_value, 314 param$show_default) 315 316 # value maps to either "value" or "selected" depending on the control. 317 if ("value" %in% inputControlFnFormals) { 318 arguments$value <<- current_value 319 } else if ("selected" %in% inputControlFnFormals) { 320 arguments$selected <<- current_value 321 } 322 } else if (name == "show_default") { 323 # No-op 324 } else { 325 ## Not a special field. Blindly promote to the input control. 326 arguments[[name]] <<- if (inherits(param[[name]], 'knit_param_expr')) { 327 param[[name]][['value']] 328 } else param[[name]] 329 } 330 }) 331 332 ## This is based on param$value not current_value because we want to 333 ## understand deviation from the report default, not any (optional) 334 ## call-time override. 335 uidefault <- params_value_to_ui(inputControlFn, param$value, param$show_default) 336 hasDefaultValue <- function(value) { 337 identical(uidefault, value) 338 } 339 340 inputControl <- NULL 341 unsupported <- setdiff(names(arguments), inputControlFnFormals) 342 if (length(unsupported) > 0) { 343 inputControl <- shiny::div(class = "form-group", 344 tags$label(class = "control-label",param$name), 345 shiny::div(paste('Cannot customize the parameter "', param$name, '" ', 346 'because the "', params_get_input(param), '" ', 347 'Shiny control does not support: ', 348 paste(unsupported, collapse = ', '), sep = ''))) 349 } else { 350 inputControl <- do.call(inputControlFn, arguments) 351 } 352 353 showSelectControl <- NULL 354 selectControl <- NULL 355 selectInputId <- paste0("select_", param$name) 356 357 ## Helper to materialize a "default/customize" control. 358 makeSelectControl <- function(default_name, custom_name) { 359 showSelectControl <<- function(current) { 360 (is.null(current) || identical(current, "default")) 361 } 362 hasDefaultValue <<- function(value) { FALSE } 363 choices <- list() 364 choices[[default_name]] <- "default" 365 choices[[custom_name]] <- "custom" 366 selectControl <<- shiny::selectInput(inputId = selectInputId, 367 label = label, 368 choices = choices) 369 } 370 371 if (is.null(params[[param$name]])) { # prior value; implicit customization 372 ## Dates and times with expressions that mean "now" or "today" are first 373 ## materialized as selects. If the user chooses to customize the field, 374 ## we then show the type-specific picker. 375 if (identical("Sys.time()", param$expr)) { 376 makeSelectControl(paste0("now (", param$value, ")"), 377 "Use a custom time") 378 } else if (identical("Sys.Date()", param$expr)) { 379 makeSelectControl(paste0("today (", param$value, ")"), 380 "Use a custom date") 381 } else if (is.null(param$value)) { 382 # fileInput defaults to null, but for other null values, ask the 383 # user to explicitly choose to override (ie. we cannot use value 384 # comparison). 385 if (!identical(inputControlFn, shiny::fileInput)) { 386 makeSelectControl("Unspecified (NULL)", 387 "Use a custom value") 388 } 389 } 390 } 391 392 output[[paste0("ui_", param$name)]] <- shiny::renderUI({ 393 # For most parameters, the selectInputId input will be NULL. 394 if (!is.null(showSelectControl) && showSelectControl(input[[selectInputId]])) { 395 selectControl 396 } else { 397 inputControl 398 } 399 }) 400 401 shiny::observe({ 402 # A little reactive magic to keep in mind. If you're in one of the 403 # "default/custom" selector scenarios, this will never fire until the 404 # user selects "custom" because the value-producing input control is 405 # not rendered until that point. 406 uivalue <- input[[param$name]] 407 if (is.null(uivalue) || hasDefaultValue(uivalue)) { 408 values[[param$name]] <<- NULL 409 } else { 410 values[[param$name]] <<- params_value_from_ui(inputControlFn, param$value, uivalue) 411 } 412 }) 413 } 414 415 lapply(configurable, function(param) { 416 param.ui(param) 417 }) 418 419 shiny::observeEvent(input$save, { 420 session$onFlushed(function() { 421 session$close() 422 shiny::stopApp(values) 423 }) 424 }) 425 426 shiny::observeEvent(input$cancel, { 427 session$onFlushed(function() { 428 session$close() 429 shiny::stopApp(NULL) 430 }) 431 }) 432 } 433 434 contents <- shiny::tags$div( 435 shiny::fluidRow(shiny::column(12, lapply(configurable, function(param) { 436 shiny::uiOutput(paste0("ui_", param$name)) 437 }))), class = "container-fluid") 438 439 if (length(unconfigurable) > 0) { 440 skipped <- shiny::tags$div(shiny::tags$strong("Note:"), 441 "The following parameters cannot be customized:", 442 paste(lapply(unconfigurable, function(param) { param$name }), collapse = ", ")) 443 contents <- shiny::tagAppendChildren(contents, shiny::fluidRow(shiny::column(12, skipped))) 444 } 445 footer <- shiny::tags$div( 446 shiny::tags$div( 447 shiny::fluidRow(shiny::column(12, 448 shiny::actionButton("save", save_caption, class = "btn-primary navbar-btn pull-right"), 449 shiny::actionButton("cancel","Cancel", class = "navbar-btn pull-right"))), 450 class = "container-fluid"), 451 class = "navbar navbar-default navbar-fixed-bottom") 452 453 style <- shiny::tags$style( 454 # Our controls are wiiiiide. 455 ".container-fluid .shiny-input-container { width: auto; }", 456 # Prevent the save/cancel buttons from squashing together. 457 ".navbar button { margin-left: 10px; }", 458 # Style for the navbar footer. 459 # http://getbootstrap.com/components/#navbar-fixed-bottom 460 "body { padding-bottom: 70px; }" 461 ) 462 ## Escape is "cancel" and Enter is "save". 463 script <- shiny::tags$script(shiny::HTML("$(document).keyup(function(e) {\n", 464 "if (e.which == 13) { $('#save').click(); } // enter\n", 465 "if (e.which == 27) { $('#cancel').click(); } // esc\n", 466 "});" 467 )) 468 ui <- shiny::bootstrapPage( 469 shiny::tags$head(style, script), 470 contents, 471 footer) 472 473 shiny_app <- shiny::shinyApp(ui = ui, server = server) 474 shiny_args <- merge_lists(list(appDir = shiny_app), shiny_args) 475 do.call(shiny::runApp, shiny_args) 476} 477