1ShinySaveState <- R6Class("ShinySaveState",
2  public = list(
3    input = NULL,
4    exclude = NULL,
5    onSave = NULL, # A callback to invoke during the saving process.
6
7    # These are set not in initialize(), but by external functions that modify
8    # the ShinySaveState object.
9    dir = NULL,
10
11
12    initialize = function(input = NULL, exclude = NULL, onSave = NULL) {
13      self$input   <- input
14      self$exclude <- exclude
15      self$onSave  <- onSave
16      private$values_  <- new.env(parent = emptyenv())
17    }
18  ),
19
20  active = list(
21    # `values` looks to the outside world like an environment for storing
22    # arbitrary values. Two things to note: (1) This is an environment (instead
23    # of, say, a list) because if the onSave function represents multiple
24    # callback functions (when onBookmark is called multiple times), each
25    # callback can change `values`, and if we used a list, one of the callbacks
26    # could easily obliterate values set by another. This can happen when using
27    # modules that have an onBookmark function. (2) The purpose of the active
28    # binding is to prevent replacing state$values with another arbitrary
29    # object. (Simply locking the binding would prevent all changes to
30    # state$values.)
31    values = function(value) {
32      if (missing(value))
33        return(private$values_)
34
35      if (identical(value, private$values_)) {
36        return(value)
37      } else {
38        stop("Items in `values` can be changed, but `values` itself cannot be replaced.")
39      }
40    }
41  ),
42
43  private = list(
44    values_ = NULL
45  )
46)
47
48
49# Save a state to disk. Returns a query string which can be used to restore the
50# session.
51saveShinySaveState <- function(state) {
52  id <- createUniqueId(8)
53
54  # A function for saving the state object to disk, given a directory to save
55  # to.
56  saveState <- function(stateDir) {
57    state$dir <- stateDir
58
59    # Allow user-supplied onSave function to do things like add state$values, or
60    # save data to state dir.
61    if (!is.null(state$onSave))
62      isolate(state$onSave(state))
63
64    # Serialize values, possibly saving some extra data to stateDir
65    exclude <- c(state$exclude, "._bookmark_")
66    inputValues <- serializeReactiveValues(state$input, exclude, state$dir)
67    saveRDS(inputValues, file.path(stateDir, "input.rds"))
68
69    # If values were added, save them also.
70    if (length(state$values) != 0)
71      saveRDS(state$values, file.path(stateDir, "values.rds"))
72  }
73
74  # Pass the saveState function to the save interface function, which will
75  # invoke saveState after preparing the directory.
76
77  # Look for a save.interface function. This will be defined by the hosting
78  # environment if it supports bookmarking.
79  saveInterface <- getShinyOption("save.interface", default = NULL)
80
81  if (is.null(saveInterface)) {
82    if (inShinyServer()) {
83      # We're in a version of Shiny Server/Connect that doesn't have
84      # bookmarking support.
85      saveInterface <- function(id, callback) {
86        stop("The hosting environment does not support saved-to-server bookmarking.")
87      }
88
89    } else {
90      # We're running Shiny locally.
91      saveInterface <- saveInterfaceLocal
92    }
93  }
94
95  saveInterface(id, saveState)
96
97  paste0("_state_id_=", encodeURIComponent(id))
98}
99
100# Encode the state to a URL. This does not save to disk.
101encodeShinySaveState <- function(state) {
102  exclude <- c(state$exclude, "._bookmark_")
103  inputVals <- serializeReactiveValues(state$input, exclude, stateDir = NULL)
104
105  # Allow user-supplied onSave function to do things like add state$values.
106  if (!is.null(state$onSave))
107    isolate(state$onSave(state))
108
109  inputVals <- vapply(inputVals,
110    function(x) toJSON(x, strict_atomic = FALSE),
111    character(1),
112    USE.NAMES = TRUE
113  )
114
115  res <- ""
116
117  # If any input values are present, add them.
118  if (length(inputVals) != 0) {
119    res <- paste0(res, "_inputs_&",
120      paste0(
121        encodeURIComponent(names(inputVals)),
122        "=",
123        encodeURIComponent(inputVals),
124        collapse = "&"
125      )
126    )
127  }
128
129  # If 'values' is present, add them as well.
130  if (length(state$values) != 0) {
131    values <- vapply(state$values,
132      function(x) toJSON(x, strict_atomic = FALSE),
133      character(1),
134      USE.NAMES = TRUE
135    )
136
137    res <- paste0(res,
138      if (length(inputVals != 0)) "&",  # Add separator if there were inputs
139      "_values_&",
140      paste0(
141        encodeURIComponent(names(values)),
142        "=",
143        encodeURIComponent(values),
144        collapse = "&"
145      )
146    )
147  }
148
149  res
150}
151
152RestoreContext <- R6Class("RestoreContext",
153  public = list(
154    # This will be set to TRUE if there's actually a state to restore
155    active = FALSE,
156
157    # This is set to an error message string in case there was an initialization
158    # error. Later, after the app has started on the client, the server can send
159    # this message as a notification on the client.
160    initErrorMessage = NULL,
161
162    # This is a RestoreInputSet for input values. This is a key-value store with
163    # some special handling.
164    input = NULL,
165
166    # Directory for extra files, if restoring from state that was saved to disk.
167    dir = NULL,
168
169    # For values other than input values. These values don't need the special
170    # phandling that's needed for input values, because they're only accessed
171    # from the onRestore function.
172    values = NULL,
173
174    initialize = function(queryString = NULL) {
175      self$reset() # Need this to initialize self$input
176
177      if (!is.null(queryString) && nzchar(queryString)) {
178        tryCatch(
179          withLogErrors({
180            qsValues <- parseQueryString(queryString, nested = TRUE)
181
182            if (!is.null(qsValues[["__subapp__"]]) && qsValues[["__subapp__"]] == 1) {
183              # Ignore subapps in shiny docs
184              self$reset()
185
186            } else if (!is.null(qsValues[["_state_id_"]]) && nzchar(qsValues[["_state_id_"]])) {
187              # If we have a "_state_id_" key, restore from saved state and
188              # ignore other key/value pairs. If not, restore from key/value
189              # pairs in the query string.
190              self$active <- TRUE
191              private$loadStateQueryString(queryString)
192
193            } else {
194              # The query string contains the saved keys and values
195              self$active <- TRUE
196              private$decodeStateQueryString(queryString)
197            }
198          }),
199          error = function(e) {
200            # If there's an error in restoring problem, just reset these values
201            self$reset()
202            self$initErrorMessage <- e$message
203            warning(e$message)
204          }
205        )
206      }
207    },
208
209    reset = function() {
210      self$active <- FALSE
211      self$initErrorMessage <- NULL
212      self$input <- RestoreInputSet$new(list())
213      self$values <- new.env(parent = emptyenv())
214      self$dir <- NULL
215    },
216
217    # Completely replace the state
218    set = function(active = FALSE, initErrorMessage = NULL, input = list(), values = list(), dir = NULL) {
219      # Validate all inputs
220      stopifnot(is.logical(active))
221      stopifnot(is.null(initErrorMessage) || is.character(initErrorMessage))
222      stopifnot(is.list(input))
223      stopifnot(is.list(values))
224      stopifnot(is.null(dir) || is.character(dir))
225
226      self$active <- active
227      self$initErrorMessage <- initErrorMessage
228      self$input <- RestoreInputSet$new(input)
229      self$values <- list2env2(values, parent = emptyenv())
230      self$dir <- dir
231    },
232
233    # This should be called before a restore context is popped off the stack.
234    flushPending = function() {
235      self$input$flushPending()
236    },
237
238
239    # Returns a list representation of the RestoreContext object. This is passed
240    # to the app author's onRestore function. An important difference between
241    # the RestoreContext object and the list is that the former's `input` field
242    # is a RestoreInputSet object, while the latter's `input` field is just a
243    # list.
244    asList = function() {
245      list(
246        input = self$input$asList(),
247        dir = self$dir,
248        values = self$values
249      )
250    }
251  ),
252
253  private = list(
254    # Given a query string with a _state_id_, load saved state with that ID.
255    loadStateQueryString = function(queryString) {
256      values <- parseQueryString(queryString, nested = TRUE)
257      id <- values[["_state_id_"]]
258
259      # Check that id has only alphanumeric chars
260      if (grepl("[^a-zA-Z0-9]", id)) {
261        stop("Invalid state id: ", id)
262      }
263
264      # This function is passed to the loadInterface function; given a
265      # directory, it will load state from that directory
266      loadFun <- function(stateDir) {
267        self$dir <- stateDir
268
269        if (!dirExists(stateDir)) {
270          stop("Bookmarked state directory does not exist.")
271        }
272
273        tryCatch({
274            inputValues <- readRDS(file.path(stateDir, "input.rds"))
275            self$input <- RestoreInputSet$new(inputValues)
276          },
277          error = function(e) {
278            stop("Error reading input values file.")
279          }
280        )
281
282        valuesFile <- file.path(stateDir, "values.rds")
283        if (file.exists(valuesFile)) {
284          tryCatch({
285              self$values <- readRDS(valuesFile)
286            },
287            error = function(e) {
288              stop("Error reading values file.")
289            }
290          )
291        }
292      }
293
294      # Look for a load.interface function. This will be defined by the hosting
295      # environment if it supports bookmarking.
296      loadInterface <- getShinyOption("load.interface", default = NULL)
297
298      if (is.null(loadInterface)) {
299        if (inShinyServer()) {
300          # We're in a version of Shiny Server/Connect that doesn't have
301          # bookmarking support.
302          loadInterface <- function(id, callback) {
303            stop("The hosting environment does not support saved-to-server bookmarking.")
304          }
305
306        } else {
307          # We're running Shiny locally.
308          loadInterface <- loadInterfaceLocal
309        }
310      }
311
312      loadInterface(id, loadFun)
313
314      invisible()
315    },
316
317    # Given a query string with values encoded in it, restore saved state
318    # from those values.
319    decodeStateQueryString = function(queryString) {
320      # Remove leading '?'
321      if (substr(queryString, 1, 1) == '?')
322        queryString <- substr(queryString, 2, nchar(queryString))
323
324
325      # Error if multiple '_inputs_' or '_values_'. This is needed because
326      # strsplit won't add an entry if the search pattern is at the end of a
327      # string.
328      if (length(gregexpr("(^|&)_inputs_(&|$)", queryString)[[1]]) > 1)
329        stop("Invalid state string: more than one '_inputs_' found")
330      if (length(gregexpr("(^|&)_values_(&|$)", queryString)[[1]]) > 1)
331        stop("Invalid state string: more than one '_values_' found")
332
333      # Look for _inputs_ and store following content in inputStr
334      splitStr <- strsplit(queryString, "(^|&)_inputs_(&|$)")[[1]]
335      if (length(splitStr) == 2) {
336        inputStr <- splitStr[2]
337        # Remove any _values_ (and content after _values_) that may come after
338        # _inputs_
339        inputStr <- strsplit(inputStr, "(^|&)_values_(&|$)")[[1]][1]
340
341      } else {
342        inputStr <- ""
343      }
344
345      # Look for _values_ and store following content in valueStr
346      splitStr <- strsplit(queryString, "(^|&)_values_(&|$)")[[1]]
347      if (length(splitStr) == 2) {
348        valueStr <- splitStr[2]
349        # Remove any _inputs_ (and content after _inputs_) that may come after
350        # _values_
351        valueStr <- strsplit(valueStr, "(^|&)_inputs_(&|$)")[[1]][1]
352
353      } else {
354        valueStr <- ""
355      }
356
357
358      inputs <- parseQueryString(inputStr, nested = TRUE)
359      values <- parseQueryString(valueStr, nested = TRUE)
360
361      valuesFromJSON <- function(vals) {
362        mapply(names(vals), vals, SIMPLIFY = FALSE,
363          FUN = function(name, value) {
364            tryCatch(
365              safeFromJSON(value),
366              error = function(e) {
367                stop("Failed to parse URL parameter \"", name, "\"")
368              }
369            )
370          }
371        )
372      }
373
374      inputs <- valuesFromJSON(inputs)
375      self$input <- RestoreInputSet$new(inputs)
376
377      values <- valuesFromJSON(values)
378      self$values <- list2env2(values, self$values)
379    }
380  )
381)
382
383
384# Restore input set. This is basically a key-value store, except for one
385# important difference: When the user `get()`s a value, the value is marked as
386# pending; when `flushPending()` is called, those pending values are marked as
387# used. When a value is marked as used, `get()` will not return it, unless
388# called with `force=TRUE`. This is to make sure that a particular value can be
389# restored only within a single call to `withRestoreContext()`. Without this, if
390# a value is restored in a dynamic UI, it could completely prevent any other
391# (non- restored) kvalue from being used.
392RestoreInputSet <- R6Class("RestoreInputSet",
393  private = list(
394    values = NULL,
395    pending = character(0),
396    used = character(0)     # Names of values which have been used
397  ),
398
399  public = list(
400    initialize = function(values) {
401      private$values <- list2env2(values, parent = emptyenv())
402    },
403
404    exists = function(name) {
405      exists(name, envir = private$values)
406    },
407
408    # Return TRUE if the value exists and has not been marked as used.
409    available = function(name) {
410      self$exists(name) && !self$isUsed(name)
411    },
412
413    isPending = function(name) {
414      name %in% private$pending
415    },
416
417    isUsed = function(name) {
418      name %in% private$used
419    },
420
421    # Get a value. If `force` is TRUE, get the value without checking whether
422    # has been used, and without marking it as pending.
423    get = function(name, force = FALSE) {
424      if (force)
425        return(private$values[[name]])
426
427      if (!self$available(name))
428        return(NULL)
429
430      # Mark this name as pending. Use unique so that it's not added twice.
431      private$pending <- unique(c(private$pending, name))
432      private$values[[name]]
433    },
434
435    # Take pending names and mark them as used, then clear pending list.
436    flushPending = function() {
437      private$used <- unique(c(private$used, private$pending))
438      private$pending <- character(0)
439    },
440
441    asList = function() {
442      as.list.environment(private$values, all.names = TRUE)
443    }
444  )
445)
446
447# This is a fastmap::faststack(); value is assigned in .onLoad().
448restoreCtxStack <- NULL
449
450withRestoreContext <- function(ctx, expr) {
451  restoreCtxStack$push(ctx)
452
453  on.exit({
454    # Mark pending names as used
455    restoreCtxStack$peek()$flushPending()
456    restoreCtxStack$pop()
457  }, add = TRUE)
458
459  force(expr)
460}
461
462# Is there a current restore context?
463hasCurrentRestoreContext <- function() {
464  if (restoreCtxStack$size() > 0)
465    return(TRUE)
466  domain <- getDefaultReactiveDomain()
467  if (!is.null(domain) && !is.null(domain$restoreContext))
468    return(TRUE)
469
470  return(FALSE)
471}
472
473# Call to access the current restore context. First look on the restore
474# context stack, and if not found, then see if there's one on the current
475# reactive domain. In practice, the only time there will be a restore context
476# on the stack is when executing the UI function; when executing server code,
477# the restore context will be attached to the domain/session.
478getCurrentRestoreContext <- function() {
479  ctx <- restoreCtxStack$peek()
480  if (is.null(ctx)) {
481    domain <- getDefaultReactiveDomain()
482
483    if (is.null(domain) || is.null(domain$restoreContext)) {
484      stop("No restore context found")
485    }
486
487    ctx <- domain$restoreContext
488  }
489  ctx
490}
491
492#' Restore an input value
493#'
494#' This restores an input value from the current restore context. It should be
495#' called early on inside of input functions (like [textInput()]).
496#'
497#' @param id Name of the input value to restore.
498#' @param default A default value to use, if there's no value to restore.
499#'
500#' @export
501restoreInput <- function(id, default) {
502  # Need to evaluate `default` in case it contains reactives like input$x. If we
503  # don't, then the calling code won't take a reactive dependency on input$x
504  # when restoring a value.
505  force(default)
506
507  if (!hasCurrentRestoreContext()) {
508    return(default)
509  }
510
511  oldInputs <- getCurrentRestoreContext()$input
512  if (oldInputs$available(id)) {
513    oldInputs$get(id)
514  } else {
515    default
516  }
517}
518
519#' Update URL in browser's location bar
520#'
521#' This function updates the client browser's query string in the location bar.
522#' It typically is called from an observer. Note that this will not work in
523#' Internet Explorer 9 and below.
524#'
525#' For `mode = "push"`, only three updates are currently allowed:
526#' \enumerate{
527#'   \item the query string (format: `?param1=val1&param2=val2`)
528#'   \item the hash (format: `#hash`)
529#'   \item both the query string and the hash
530#'     (format: `?param1=val1&param2=val2#hash`)
531#' }
532#'
533#' In other words, if `mode = "push"`, the `queryString` must start
534#' with either `?` or with `#`.
535#'
536#' A technical curiosity: under the hood, this function is calling the HTML5
537#' history API (which is where the names for the `mode` argument come from).
538#' When `mode = "replace"`, the function called is
539#' `window.history.replaceState(null, null, queryString)`.
540#' When `mode = "push"`, the function called is
541#' `window.history.pushState(null, null, queryString)`.
542#'
543#' @param queryString The new query string to show in the location bar.
544#' @param mode When the query string is updated, should the the current history
545#'   entry be replaced (default), or should a new history entry be pushed onto
546#'   the history stack? The former should only be used in a live bookmarking
547#'   context. The latter is useful if you want to navigate between states using
548#'   the browser's back and forward buttons. See Examples.
549#' @param session A Shiny session object.
550#' @seealso [enableBookmarking()], [getQueryString()]
551#' @examples
552#' ## Only run these examples in interactive sessions
553#' if (interactive()) {
554#'
555#'   ## App 1: Doing "live" bookmarking
556#'   ## Update the browser's location bar every time an input changes.
557#'   ## This should not be used with enableBookmarking("server"),
558#'   ## because that would create a new saved state on disk every time
559#'   ## the user changes an input.
560#'   enableBookmarking("url")
561#'   shinyApp(
562#'     ui = function(req) {
563#'       fluidPage(
564#'         textInput("txt", "Text"),
565#'         checkboxInput("chk", "Checkbox")
566#'       )
567#'     },
568#'     server = function(input, output, session) {
569#'       observe({
570#'         # Trigger this observer every time an input changes
571#'         reactiveValuesToList(input)
572#'         session$doBookmark()
573#'       })
574#'       onBookmarked(function(url) {
575#'         updateQueryString(url)
576#'       })
577#'     }
578#'   )
579#'
580#'   ## App 2: Printing the value of the query string
581#'   ## (Use the back and forward buttons to see how the browser
582#'   ## keeps a record of each state)
583#'   shinyApp(
584#'     ui = fluidPage(
585#'       textInput("txt", "Enter new query string"),
586#'       helpText("Format: ?param1=val1&param2=val2"),
587#'       actionButton("go", "Update"),
588#'       hr(),
589#'       verbatimTextOutput("query")
590#'     ),
591#'     server = function(input, output, session) {
592#'       observeEvent(input$go, {
593#'         updateQueryString(input$txt, mode = "push")
594#'       })
595#'       output$query <- renderText({
596#'         query <- getQueryString()
597#'         queryText <- paste(names(query), query,
598#'                        sep = "=", collapse=", ")
599#'         paste("Your query string is:\n", queryText)
600#'       })
601#'     }
602#'   )
603#' }
604#' @export
605updateQueryString <- function(queryString, mode = c("replace", "push"),
606                              session = getDefaultReactiveDomain()) {
607  mode <- match.arg(mode)
608  session$updateQueryString(queryString, mode)
609}
610
611#' Create a button for bookmarking/sharing
612#'
613#' A `bookmarkButton` is a [actionButton()] with a default label
614#' that consists of a link icon and the text "Bookmark...". It is meant to be
615#' used for bookmarking state.
616#'
617#' @inheritParams actionButton
618#' @param title A tooltip that is shown when the mouse cursor hovers over the
619#'   button.
620#' @param id An ID for the bookmark button. The only time it is necessary to set
621#'   the ID unless you have more than one bookmark button in your application.
622#'   If you specify an input ID, it should be excluded from bookmarking with
623#'   [setBookmarkExclude()], and you must create an observer that
624#'   does the bookmarking when the button is pressed. See the examples below.
625#'
626#' @seealso [enableBookmarking()] for more examples.
627#'
628#' @examples
629#' ## Only run these examples in interactive sessions
630#' if (interactive()) {
631#'
632#' # This example shows how to use multiple bookmark buttons. If you only need
633#' # a single bookmark button, see examples in ?enableBookmarking.
634#' ui <- function(request) {
635#'   fluidPage(
636#'     tabsetPanel(id = "tabs",
637#'       tabPanel("One",
638#'         checkboxInput("chk1", "Checkbox 1"),
639#'         bookmarkButton(id = "bookmark1")
640#'       ),
641#'       tabPanel("Two",
642#'         checkboxInput("chk2", "Checkbox 2"),
643#'         bookmarkButton(id = "bookmark2")
644#'       )
645#'     )
646#'   )
647#' }
648#' server <- function(input, output, session) {
649#'   # Need to exclude the buttons from themselves being bookmarked
650#'   setBookmarkExclude(c("bookmark1", "bookmark2"))
651#'
652#'   # Trigger bookmarking with either button
653#'   observeEvent(input$bookmark1, {
654#'     session$doBookmark()
655#'   })
656#'   observeEvent(input$bookmark2, {
657#'     session$doBookmark()
658#'   })
659#' }
660#' enableBookmarking(store = "url")
661#' shinyApp(ui, server)
662#' }
663#' @export
664bookmarkButton <- function(label = "Bookmark...",
665  icon = shiny::icon("link", lib = "glyphicon"),
666  title = "Bookmark this application's state and get a URL for sharing.",
667  ...,
668  id = "._bookmark_")
669{
670  actionButton(id, label, icon, title = title, ...)
671}
672
673
674#' Generate a modal dialog that displays a URL
675#'
676#' The modal dialog generated by `urlModal` will display the URL in a
677#' textarea input, and the URL text will be selected so that it can be easily
678#' copied. The result from `urlModal` should be passed to the
679#' [showModal()] function to display it in the browser.
680#'
681#' @param url A URL to display in the dialog box.
682#' @param title A title for the dialog box.
683#' @param subtitle Text to display underneath URL.
684#' @export
685urlModal <- function(url, title = "Bookmarked application link", subtitle = NULL) {
686
687  subtitleTag <- tagList(
688    br(),
689    span(class = "text-muted", subtitle),
690    span(id = "shiny-bookmark-copy-text", class = "text-muted")
691  )
692
693  modalDialog(
694    title = title,
695    easyClose = TRUE,
696    tags$textarea(class = "form-control", rows = "1", style = "resize: none;",
697      readonly = "readonly",
698      url
699    ),
700    subtitleTag,
701    # Need separate show and shown listeners. The show listener sizes the
702    # textarea just as the modal starts to fade in. The 200ms delay is needed
703    # because if we try to resize earlier, it can't calculate the text height
704    # (scrollHeight will be reported as zero). The shown listener selects the
705    # text; it's needed because because selection has to be done after the fade-
706    # in is completed.
707    tags$script(
708      "$('#shiny-modal').
709        one('show.bs.modal', function() {
710          setTimeout(function() {
711            var $textarea = $('#shiny-modal textarea');
712            $textarea.innerHeight($textarea[0].scrollHeight);
713          }, 200);
714        });
715      $('#shiny-modal')
716        .one('shown.bs.modal', function() {
717          $('#shiny-modal textarea').select().focus();
718        });
719      $('#shiny-bookmark-copy-text')
720        .text(function() {
721          if (/Mac/i.test(navigator.userAgent)) {
722            return 'Press \u2318-C to copy.';
723          } else {
724            return 'Press Ctrl-C to copy.';
725          }
726        });
727      "
728    )
729  )
730}
731
732
733#' Display a modal dialog for bookmarking
734#'
735#' This is a wrapper function for [urlModal()] that is automatically
736#' called if an application is bookmarked but no other [onBookmark()]
737#' callback was set. It displays a modal dialog with the bookmark URL, along
738#' with a subtitle that is appropriate for the type of bookmarking used ("url"
739#' or "server").
740#'
741#' @param url A URL to show in the modal dialog.
742#' @export
743showBookmarkUrlModal <- function(url) {
744  store <- getShinyOption("bookmarkStore", default = "")
745  if (store == "url") {
746    subtitle <- "This link stores the current state of this application."
747  } else if (store == "server") {
748    subtitle <- "The current state of this application has been stored on the server."
749  } else {
750    subtitle <- NULL
751  }
752
753  showModal(urlModal(url, subtitle = subtitle))
754}
755
756#' Enable bookmarking for a Shiny application
757#'
758#' @description
759#'
760#' There are two types of bookmarking: saving an application's state to disk on
761#' the server, and encoding the application's state in a URL. For state that has
762#' been saved to disk, the state can be restored with the corresponding state
763#' ID. For URL-encoded state, the state of the application is encoded in the
764#' URL, and no server-side storage is needed.
765#'
766#' URL-encoded bookmarking is appropriate for applications where there not many
767#' input values that need to be recorded. Some browsers have a length limit for
768#' URLs of about 2000 characters, and if there are many inputs, the length of
769#' the URL can exceed that limit.
770#'
771#' Saved-on-server bookmarking is appropriate when there are many inputs, or
772#' when the bookmarked state requires storing files.
773#'
774#' @details
775#'
776#' For restoring state to work properly, the UI must be a function that takes
777#' one argument, `request`. In most Shiny applications, the UI is not a
778#' function; it might have the form `fluidPage(....)`. Converting it to a
779#' function is as simple as wrapping it in a function, as in
780#' \code{function(request) \{ fluidPage(....) \}}.
781#'
782#' By default, all input values will be bookmarked, except for the values of
783#' passwordInputs. fileInputs will be saved if the state is saved on a server,
784#' but not if the state is encoded in a URL.
785#'
786#' When bookmarking state, arbitrary values can be stored, by passing a function
787#' as the `onBookmark` argument. That function will be passed a
788#' `ShinySaveState` object. The `values` field of the object is a list
789#' which can be manipulated to save extra information. Additionally, if the
790#' state is being saved on the server, and the `dir` field of that object
791#' can be used to save extra information to files in that directory.
792#'
793#' For saved-to-server state, this is how the state directory is chosen:
794#' \itemize{
795#'   \item If running in a hosting environment such as Shiny Server or
796#'     Connect, the hosting environment will choose the directory.
797#'   \item If running an app in a directory with [runApp()], the
798#'     saved states will be saved in a subdirectory of the app called
799#'    shiny_bookmarks.
800#'   \item If running a Shiny app object that is generated from code (not run
801#'     from a directory), the saved states will be saved in a subdirectory of
802#'     the current working directory called shiny_bookmarks.
803#' }
804#'
805#' When used with [shinyApp()], this function must be called before
806#' `shinyApp()`, or in the `shinyApp()`'s `onStart` function. An
807#' alternative to calling the `enableBookmarking()` function is to use the
808#' `enableBookmarking` *argument* for `shinyApp()`. See examples
809#' below.
810#'
811#' @param store Either `"url"`, which encodes all of the relevant values in
812#'   a URL, `"server"`, which saves to disk on the server, or
813#'   `"disable"`, which disables any previously-enabled bookmarking.
814#'
815#' @seealso [onBookmark()], [onBookmarked()],
816#'   [onRestore()], and [onRestored()] for registering
817#'   callback functions that are invoked when the state is bookmarked or
818#'   restored.
819#'
820#'   Also see [updateQueryString()].
821#'
822#' @export
823#' @examples
824#' ## Only run these examples in interactive R sessions
825#' if (interactive()) {
826#'
827#' # Basic example with state encoded in URL
828#' ui <- function(request) {
829#'   fluidPage(
830#'     textInput("txt", "Text"),
831#'     checkboxInput("chk", "Checkbox"),
832#'     bookmarkButton()
833#'   )
834#' }
835#' server <- function(input, output, session) { }
836#' enableBookmarking("url")
837#' shinyApp(ui, server)
838#'
839#'
840#' # An alternative to calling enableBookmarking(): use shinyApp's
841#' # enableBookmarking argument
842#' shinyApp(ui, server, enableBookmarking = "url")
843#'
844#'
845#' # Same basic example with state saved to disk
846#' enableBookmarking("server")
847#' shinyApp(ui, server)
848#'
849#'
850#' # Save/restore arbitrary values
851#' ui <- function(req) {
852#'   fluidPage(
853#'     textInput("txt", "Text"),
854#'     checkboxInput("chk", "Checkbox"),
855#'     bookmarkButton(),
856#'     br(),
857#'     textOutput("lastSaved")
858#'   )
859#' }
860#' server <- function(input, output, session) {
861#'   vals <- reactiveValues(savedTime = NULL)
862#'   output$lastSaved <- renderText({
863#'     if (!is.null(vals$savedTime))
864#'       paste("Last saved at", vals$savedTime)
865#'     else
866#'       ""
867#'   })
868#'
869#'   onBookmark(function(state) {
870#'     vals$savedTime <- Sys.time()
871#'     # state is a mutable reference object, and we can add arbitrary values
872#'     # to it.
873#'     state$values$time <- vals$savedTime
874#'   })
875#'   onRestore(function(state) {
876#'     vals$savedTime <- state$values$time
877#'   })
878#' }
879#' enableBookmarking(store = "url")
880#' shinyApp(ui, server)
881#'
882#'
883#' # Usable with dynamic UI (set the slider, then change the text input,
884#' # click the bookmark button)
885#' ui <- function(request) {
886#'   fluidPage(
887#'     sliderInput("slider", "Slider", 1, 100, 50),
888#'     uiOutput("ui"),
889#'     bookmarkButton()
890#'   )
891#' }
892#' server <- function(input, output, session) {
893#'   output$ui <- renderUI({
894#'     textInput("txt", "Text", input$slider)
895#'   })
896#' }
897#' enableBookmarking("url")
898#' shinyApp(ui, server)
899#'
900#'
901#' # Exclude specific inputs (The only input that will be saved in this
902#' # example is chk)
903#' ui <- function(request) {
904#'   fluidPage(
905#'     passwordInput("pw", "Password"), # Passwords are never saved
906#'     sliderInput("slider", "Slider", 1, 100, 50), # Manually excluded below
907#'     checkboxInput("chk", "Checkbox"),
908#'     bookmarkButton()
909#'   )
910#' }
911#' server <- function(input, output, session) {
912#'   setBookmarkExclude("slider")
913#' }
914#' enableBookmarking("url")
915#' shinyApp(ui, server)
916#'
917#'
918#' # Update the browser's location bar every time an input changes. This should
919#' # not be used with enableBookmarking("server"), because that would create a
920#' # new saved state on disk every time the user changes an input.
921#' ui <- function(req) {
922#'   fluidPage(
923#'     textInput("txt", "Text"),
924#'     checkboxInput("chk", "Checkbox")
925#'   )
926#' }
927#' server <- function(input, output, session) {
928#'   observe({
929#'     # Trigger this observer every time an input changes
930#'     reactiveValuesToList(input)
931#'     session$doBookmark()
932#'   })
933#'   onBookmarked(function(url) {
934#'     updateQueryString(url)
935#'   })
936#' }
937#' enableBookmarking("url")
938#' shinyApp(ui, server)
939#'
940#'
941#' # Save/restore uploaded files
942#' ui <- function(request) {
943#'   fluidPage(
944#'     sidebarLayout(
945#'       sidebarPanel(
946#'         fileInput("file1", "Choose CSV File", multiple = TRUE,
947#'           accept = c(
948#'             "text/csv",
949#'             "text/comma-separated-values,text/plain",
950#'             ".csv"
951#'           )
952#'         ),
953#'         tags$hr(),
954#'         checkboxInput("header", "Header", TRUE),
955#'         bookmarkButton()
956#'       ),
957#'       mainPanel(
958#'         tableOutput("contents")
959#'       )
960#'     )
961#'   )
962#' }
963#' server <- function(input, output) {
964#'   output$contents <- renderTable({
965#'     inFile <- input$file1
966#'     if (is.null(inFile))
967#'       return(NULL)
968#'
969#'     if (nrow(inFile) == 1) {
970#'       read.csv(inFile$datapath, header = input$header)
971#'     } else {
972#'       data.frame(x = "multiple files")
973#'     }
974#'   })
975#' }
976#' enableBookmarking("server")
977#' shinyApp(ui, server)
978#'
979#' }
980enableBookmarking <- function(store = c("url", "server", "disable")) {
981  store <- match.arg(store)
982  shinyOptions(bookmarkStore = store)
983}
984
985
986#' Exclude inputs from bookmarking
987#'
988#' This function tells Shiny which inputs should be excluded from bookmarking.
989#' It should be called from inside the application's server function.
990#'
991#' This function can also be called from a module's server function, in which
992#' case it will exclude inputs with the specified names, from that module. It
993#' will not affect inputs from other modules or from the top level of the Shiny
994#' application.
995#'
996#' @param names A character vector containing names of inputs to exclude from
997#'   bookmarking.
998#' @param session A shiny session object.
999#' @seealso [enableBookmarking()] for examples.
1000#' @export
1001setBookmarkExclude <- function(names = character(0), session = getDefaultReactiveDomain()) {
1002  session$setBookmarkExclude(names)
1003}
1004
1005
1006#' Add callbacks for Shiny session bookmarking events
1007#'
1008#' @description
1009#'
1010#' These functions are for registering callbacks on Shiny session events. They
1011#' should be called within an application's server function.
1012#'
1013#' \itemize{
1014#'   \item `onBookmark` registers a function that will be called just
1015#'     before Shiny bookmarks state.
1016#'   \item `onBookmarked` registers a function that will be called just
1017#'     after Shiny bookmarks state.
1018#'   \item `onRestore` registers a function that will be called when a
1019#'     session is restored, after the server function executes, but before all
1020#'     other reactives, observers and render functions are run.
1021#'   \item `onRestored` registers a function that will be called after a
1022#'     session is restored. This is similar to `onRestore`, but it will be
1023#'     called after all reactives, observers, and render functions run, and
1024#'     after results are sent to the client browser. `onRestored`
1025#'     callbacks can be useful for sending update messages to the client
1026#'     browser.
1027#' }
1028#'
1029#' @details
1030#'
1031#' All of these functions return a function which can be called with no
1032#' arguments to cancel the registration.
1033#'
1034#' The callback function that is passed to these functions should take one
1035#' argument, typically named "state" (for `onBookmark`, `onRestore`,
1036#' and `onRestored`) or "url" (for `onBookmarked`).
1037#'
1038#' For `onBookmark`, the state object has three relevant fields. The
1039#' `values` field is an environment which can be used to save arbitrary
1040#' values (see examples). If the state is being saved to disk (as opposed to
1041#' being encoded in a URL), the `dir` field contains the name of a
1042#' directory which can be used to store extra files. Finally, the state object
1043#' has an `input` field, which is simply the application's `input`
1044#' object. It can be read, but not modified.
1045#'
1046#' For `onRestore` and `onRestored`, the state object is a list. This
1047#' list contains `input`, which is a named list of input values to restore,
1048#' `values`, which is an environment containing arbitrary values that were
1049#' saved in `onBookmark`, and `dir`, the name of the directory that
1050#' the state is being restored from, and which could have been used to save
1051#' extra files.
1052#'
1053#' For `onBookmarked`, the callback function receives a string with the
1054#' bookmark URL. This callback function should be used to display UI in the
1055#' client browser with the bookmark URL. If no callback function is registered,
1056#' then Shiny will by default display a modal dialog with the bookmark URL.
1057#'
1058#' @section Modules:
1059#'
1060#'   These callbacks may also be used in Shiny modules. When used this way, the
1061#'   inputs and values will automatically be namespaced for the module, and the
1062#'   callback functions registered for the module will only be able to see the
1063#'   module's inputs and values.
1064#'
1065#' @param fun A callback function which takes one argument.
1066#' @param session A shiny session object.
1067#' @seealso enableBookmarking for general information on bookmarking.
1068#'
1069#' @examples
1070#' ## Only run these examples in interactive sessions
1071#' if (interactive()) {
1072#'
1073#' # Basic use of onBookmark and onRestore: This app saves the time in its
1074#' # arbitrary values, and restores that time when the app is restored.
1075#' ui <- function(req) {
1076#'   fluidPage(
1077#'     textInput("txt", "Input text"),
1078#'     bookmarkButton()
1079#'   )
1080#' }
1081#' server <- function(input, output) {
1082#'   onBookmark(function(state) {
1083#'     savedTime <- as.character(Sys.time())
1084#'     cat("Last saved at", savedTime, "\n")
1085#'     # state is a mutable reference object, and we can add arbitrary values to
1086#'     # it.
1087#'     state$values$time <- savedTime
1088#'   })
1089#'
1090#'   onRestore(function(state) {
1091#'     cat("Restoring from state bookmarked at", state$values$time, "\n")
1092#'   })
1093#' }
1094#' enableBookmarking("url")
1095#' shinyApp(ui, server)
1096#'
1097#'
1098#'
1099# This app illustrates two things: saving values in a file using state$dir, and
1100# using an onRestored callback to call an input updater function. (In real use
1101# cases, it probably makes sense to save content to a file only if it's much
1102# larger.)
1103#' ui <- function(req) {
1104#'   fluidPage(
1105#'     textInput("txt", "Input text"),
1106#'     bookmarkButton()
1107#'   )
1108#' }
1109#' server <- function(input, output, session) {
1110#'   lastUpdateTime <- NULL
1111#'
1112#'   observeEvent(input$txt, {
1113#'     updateTextInput(session, "txt",
1114#'       label = paste0("Input text (Changed ", as.character(Sys.time()), ")")
1115#'     )
1116#'   })
1117#'
1118#'   onBookmark(function(state) {
1119#'     # Save content to a file
1120#'     messageFile <- file.path(state$dir, "message.txt")
1121#'     cat(as.character(Sys.time()), file = messageFile)
1122#'   })
1123#'
1124#'   onRestored(function(state) {
1125#'     # Read the file
1126#'     messageFile <- file.path(state$dir, "message.txt")
1127#'     timeText <- readChar(messageFile, 1000)
1128#'
1129#'     # updateTextInput must be called in onRestored, as opposed to onRestore,
1130#'     # because onRestored happens after the client browser is ready.
1131#'     updateTextInput(session, "txt",
1132#'       label = paste0("Input text (Changed ", timeText, ")")
1133#'     )
1134#'   })
1135#' }
1136#' # "server" bookmarking is needed for writing to disk.
1137#' enableBookmarking("server")
1138#' shinyApp(ui, server)
1139#'
1140#'
1141#' # This app has a module, and both the module and the main app code have
1142#' # onBookmark and onRestore functions which write and read state$values$hash. The
1143#' # module's version of state$values$hash does not conflict with the app's version
1144#' # of state$values$hash.
1145#' #
1146#' # A basic module that captializes text.
1147#' capitalizerUI <- function(id) {
1148#'   ns <- NS(id)
1149#'   wellPanel(
1150#'     h4("Text captializer module"),
1151#'     textInput(ns("text"), "Enter text:"),
1152#'     verbatimTextOutput(ns("out"))
1153#'   )
1154#' }
1155#' capitalizerServer <- function(input, output, session) {
1156#'   output$out <- renderText({
1157#'     toupper(input$text)
1158#'   })
1159#'   onBookmark(function(state) {
1160#'     state$values$hash <- rlang::hash(input$text)
1161#'   })
1162#'   onRestore(function(state) {
1163#'     if (identical(rlang::hash(input$text), state$values$hash)) {
1164#'       message("Module's input text matches hash ", state$values$hash)
1165#'     } else {
1166#'       message("Module's input text does not match hash ", state$values$hash)
1167#'     }
1168#'   })
1169#' }
1170#' # Main app code
1171#' ui <- function(request) {
1172#'   fluidPage(
1173#'     sidebarLayout(
1174#'       sidebarPanel(
1175#'         capitalizerUI("tc"),
1176#'         textInput("text", "Enter text (not in module):"),
1177#'         bookmarkButton()
1178#'       ),
1179#'       mainPanel()
1180#'     )
1181#'   )
1182#' }
1183#' server <- function(input, output, session) {
1184#'   callModule(capitalizerServer, "tc")
1185#'   onBookmark(function(state) {
1186#'     state$values$hash <- rlang::hash(input$text)
1187#'   })
1188#'   onRestore(function(state) {
1189#'     if (identical(rlang::hash(input$text), state$values$hash)) {
1190#'       message("App's input text matches hash ", state$values$hash)
1191#'     } else {
1192#'       message("App's input text does not match hash ", state$values$hash)
1193#'     }
1194#'   })
1195#' }
1196#' enableBookmarking(store = "url")
1197#' shinyApp(ui, server)
1198#' }
1199#' @export
1200onBookmark <- function(fun, session = getDefaultReactiveDomain()) {
1201  session$onBookmark(fun)
1202}
1203
1204#' @rdname onBookmark
1205#' @export
1206onBookmarked <- function(fun, session = getDefaultReactiveDomain()) {
1207  session$onBookmarked(fun)
1208}
1209
1210#' @rdname onBookmark
1211#' @export
1212onRestore <- function(fun, session = getDefaultReactiveDomain()) {
1213  session$onRestore(fun)
1214}
1215
1216#' @rdname onBookmark
1217#' @export
1218onRestored <- function(fun, session = getDefaultReactiveDomain()) {
1219  session$onRestored(fun)
1220}
1221