R/utils.R

Defines functions get_parent_args app_container waiterShowOnLoad extractSocialItem setBoxClass setBoxStyle createBoxTools validateBoxProps tagInsertChild rd_color_tag validateStatusPlus validateColor validateNuance validateStatus dropNulls findAttribute validateTabName hasCssClass tagAssert

Documented in app_container tagAssert

#' Assert that a tag has specified properties
#' @param tag A tag object.
#' @param type The type of a tag, like "div", "a", "span".
#' @param class An HTML class.
#' @param allowUI If TRUE (the default), allow dynamic outputs generated by
#'   \code{\link[shiny]{uiOutput}} or \code{\link[shiny]{htmlOutput}}. When a
#'   dynamic output is provided, \code{tagAssert} won't try to validate the the
#'   contents.
#' @keywords internal
tagAssert <- function(tag, type = NULL, class = NULL, allowUI = TRUE) {
  if (!inherits(tag, "shiny.tag")) {
    print(tag)
    stop("Expected an object with class 'shiny.tag'.")
  }
  
  # Skip dynamic output elements
  if (allowUI &&
      (hasCssClass(tag, "shiny-html-output") ||
       hasCssClass(tag, "shinydashboard-menu-output"))) {
    return()
  }
  
  if (!is.null(type) && tag$name != type) {
    stop("Expected tag to be of type ", type)
  }
  
  if (!is.null(class)) {
    if (is.null(tag$attribs$class)) {
      stop("Expected tag to have class '", class, "'")
      
    } else {
      tagClasses <- strsplit(tag$attribs$class, " ")[[1]]
      if (!(class %in% tagClasses)) {
        stop("Expected tag to have class '", class, "'")
      }
    }
  }
}


hasCssClass <- function(tag, class) {
  if (is.null(tag$attribs) || is.null(tag$attribs$class))
    return(FALSE)
  
  classes <- strsplit(tag$attribs$class, " +")[[1]]
  return(class %in% classes)
}


# Make sure a tab name is valid (there's no "." in it).
validateTabName <- function(name) {
  if (grepl(".", name, fixed = TRUE)) {
    stop("tabName must not have a '.' in it.")
  }
}



# This function takes a DOM element/tag object and reccurs within it until
# it finds a child which has an attribute called `attr` and with value `val`
# (and returns TRUE). If it finds an element with an attribute called `attr`
# whose value is NOT `val`, it returns FALSE. If it exhausts all children
# and it doesn't find an element with an attribute called `attr`, it also
# returns FALSE
findAttribute <- function(x, attr, val) {
  if (is.atomic(x)) return(FALSE) # exhausted this branch of the tree
  
  if (!is.null(x$attribs[[attr]])) { # found attribute called `attr`
    if (identical(x$attribs[[attr]], val)) return(TRUE)
    else return(FALSE)
  }
  
  if (length(x$children) > 0) { # recursion
    return(any(unlist(lapply(x$children, findAttribute, attr, val))))
  }
  
  return(FALSE) # found no attribute called `attr`
}

"%OR%" <- function(a, b) if (!is.null(a)) a else b

dropNulls <- function(x) {
  x[!vapply(x, is.null, FUN.VALUE = logical(1))]
}



# Returns TRUE if a status is valid; throws error otherwise.
validateStatus <- function(status) {
  
  if (status %in% validStatuses) {
    return(TRUE)
  }
  
  stop("Invalid status: ", status, ". Valid statuses are: ",
       paste(validStatuses, collapse = ", "), ".")
}


#' Valid statuses
#'
#' These status strings correspond to colors as defined in Bootstrap's CSS.
#' Although the colors can vary depending on the particular CSS selector, they
#' generally appear as follows:
#'
#' \itemize{
#'   \item \code{primary} Blue (sometimes dark blue)
#'   \item \code{secondary} Light gray
#'   \item \code{info} Blue
#'   \item \code{success} Green
#'   \item \code{warning} Orange
#'   \item \code{danger} Red
#' }
#'
#' @usage NULL
#' @format NULL
#'
#' @keywords internal
validStatuses <- c("primary", "secondary", "info", "success", "warning", "danger")




# Returns TRUE if a nuance is valid; throws error otherwise.
validateNuance <- function(nuance) {
  
  if (nuance %in% validNuances) {
    return(TRUE)
  }
  
  stop("Invalid nuance: ", nuance, ". Valid nuances are: ",
       paste(validNuances, collapse = ", "), ".")
}


#' Valid nuances
#'
#' These nuances strings correspond to colors as defined in AdminLTE's CSS.
#' Although the colors can vary depending on the particular CSS selector, they
#' generally appear as follows:
#'
#' \itemize{
#'   \item \code{gray-dark} Gray dark
#'   \item \code{gray} Gray
#'   \item \code{white} White
#' }
#'
#' @usage NULL
#' @format NULL
#'
#' @keywords internal
validNuances <- c("gray-dark", "gray", "white")




# Returns TRUE if a color is a valid color defined in AdminLTE, throws error
# otherwise.
validateColor <- function(color) {
  if (color %in% validColors) {
    return(TRUE)
  }
  
  stop("Invalid color: ", color, ". Valid colors are: ",
       paste(validColors, collapse = ", "), ".")
}

#' Valid colors
#'
#' These are valid colors for various dashboard components. Valid colors are
#' listed below:
#'
#' \itemize{
#'   \item \code{indigo} Indigo
#'   \item \code{lightblue} Light blue
#'   \item \code{navy} Dark Grey/Blue
#'   \item \code{purple} Purple
#'   \item \code{fuchsia} Fuchsia
#'   \item \code{pink} Pink
#'   \item \code{maroon} Pink
#'   \item \code{orange} Orange
#'   \item \code{lime} Light green
#'   \item \code{teal} Blue/Green
#'   \item \code{olive} Pastel green
#' }
#'
#' @usage NULL
#' @format NULL
#'
#' @keywords internal
validColors <- c("indigo", "lightblue", "navy", "purple", "fuchsia", "pink", 
                 "maroon", "orange", "lime", "teal", "olive")



# Returns TRUE if a status is valid; throws error otherwise.
validateStatusPlus <- function(status) {
  
  if (status %in% validStatusesPlus) {
    return(TRUE)
  }
  
  stop("Invalid status: ", status, ". Valid statuses are: ",
       paste(validStatusesPlus, collapse = ", "), ".")
}


#' Valid statuses extra
#' @usage NULL
#' @format NULL
#'
#' @keywords internal
validStatusesPlus <- c(validStatuses, validNuances, validColors)




# used to generate color tags in the documentation
rd_color_tag <- function(color, label = color) {
  style <- sprintf(
    "width:12px;height:12px;background:%s;border-radius:2px;display:inline-block;margin-right:5px;",
    color
  )
  sprintf(
    "\\ifelse{html}{\\out{<span style='%s'></span>%s}}{%s}",
    style, label, label
  )
}


# Insert HTML tag at any position
tagInsertChild <- function(tag, child, position) {
  tag$children <- append(tag$children, list(child), position - 1)
  tag
}


# Tool to validate the card props
validateBoxProps <- function(title, label, sidebar, dropdownMenu, status, gradient, collapsible, 
                             collapsed, solidHeader, background, elevation, width) {
  
  if (!is.null(status)) validateStatusPlus(status)
  if (!is.null(background)) validateStatusPlus(background)
  
  
  if (!collapsible & collapsed) {
    stop("Cannot collapse a card that is not collapsible.")
  }
  
  if (!is.null(status) && !is.null(background) && !solidHeader) {
    stop("solidHeader must be TRUE whenever background and status are not NULL at the same time.")
  }
  if (gradient && is.null(background)) stop("gradient cannot be used when background is NULL.")
  
  
  if (!is.null(elevation)) {
    stopifnot(is.numeric(elevation))
    stopifnot(elevation < 6)
    stopifnot(elevation >= 0)
  }
  
  if (!is.null(width)) {
    stopifnot(is.numeric(width))
    # respect the bootstrap grid
    stopifnot(width <= 12)
    stopifnot(width >= 0)
  }
}



# create box icons and return a list of icons
createBoxTools <- function(collapsible, collapsed, closable, maximizable, 
                           sidebar, dropdownMenu, boxToolSize, status, 
                           background, solidHeader) {

    btnClass <- paste0(
      "btn btn-tool", 
      if (!is.null(boxToolSize)) paste0(" btn-", boxToolSize)
    )

    if (is.null(status) && !is.null(background)) {
      btnClass <- paste0(
        btnClass,
        if (background %in% validStatusesPlus) {
          paste0(" bg-", background)
        }
      )
    }
    
    # status has always priority compared to background
    if (!is.null(status) &&  solidHeader) {
      btnClass <- paste0(
        btnClass,
        if (status %in% validStatuses) {
          paste0(" btn-", status)
        }
      )
    }

    collapseTag <- NULL
    if (collapsible) {
      collapseIcon <- if (collapsed) 
        "plus"
      else "minus"
      collapseTag <- shiny::tags$button(
        class = btnClass, 
        type = "button",
        `data-card-widget` = "collapse", 
        shiny::icon(collapseIcon)
      )
    }

    closableTag <- NULL
    if (closable) {
      closableTag <- shiny::tags$button(
        class = btnClass, 
        `data-card-widget` = "remove", 
        type = "button",
        shiny::icon("xmark")
      )
    } 

    maximizableTag <- NULL
    if (maximizable) {
      maximizableTag <- shiny::tags$button(
        type = "button",
        class = btnClass,
        `data-card-widget` = "maximize",
        shiny::icon("up-right-and-down-left-from-center")
      )
    }

    sidebarToolTag <- NULL
    if (!is.null(sidebar)) {
      sidebar[[1]]$attribs$class <- btnClass
      sidebarToolTag <- sidebar[[1]]
    }

    dropdownMenuToolTag <- NULL
    if (!is.null(dropdownMenu)) {
      dropdownMenu$children[[1]]$attribs$class <- paste0(btnClass, " dropdown-toggle")
      dropdownMenuToolTag <- dropdownMenu
    }

    dropNulls(list(dropdownMenuToolTag, collapseTag, closableTag, maximizableTag, sidebarToolTag))
  }


setBoxStyle <- function(height, sidebar) {
  style <- NULL
  if (!is.null(height)) {
    style <- paste0("height: ", shiny::validateCssUnit(height))
  }
  # add padding if box sidebar
  if (!is.null(sidebar)) {
    style <- paste0(style, "; padding: 10px;")
  }
  style
}


setBoxClass <- function(status, solidHeader, collapsible, collapsed,
elevation, gradient, background, sidebar) {
  cardCl <- "card bs4Dash"

  if (!is.null(status)) {
    cardCl <- paste0(cardCl, " card-", status)
  }

  if (!solidHeader) cardCl <- paste0(cardCl, " card-outline")

  if (collapsible && collapsed) cardCl <- paste0(cardCl, " collapsed-card")
  if (!is.null(elevation)) cardCl <- paste0(cardCl, " elevation-", elevation)

  if (!is.null(background)) {
    cardCl <- paste0(cardCl, " bg-", if (gradient) "gradient-", background)
  }


  if (!is.null(sidebar)) {
    sidebarToggle <- sidebar[[1]]
    startOpen <- sidebarToggle$attribs$`data-start-open`
    if (startOpen == "true") {
      cardCl <- paste0(cardCl, " direct-chat direct-chat-contacts-open")
    } else {
      cardCl <- paste0(cardCl, " direct-chat")
    }
  }

  cardCl
}


# extract social item in socialBox
extractSocialItem <- function(items, isComment = TRUE) {

  if (length(items) > 0) {
    dropNulls(lapply(items, function(item) {
      if (inherits(item, "list")) {
        lapply(item, function(nested) {
          cond <- if (isComment) {
            inherits(nested, "card-comment")
          } else {
            !inherits(nested, "card-comment")
          }
          if (cond) nested
        })
      } else {
        cond <- if (isComment) {
          inherits(item, "card-comment")
        } else {
          !inherits(item, "card-comment")
        }
        if (cond) item
      }
    }))
  } else {
    NULL
  }
}






randomInt <- function (min, max) {
  if (missing(max)) {
    max <- min
    min <- 0
  }
  if (min < 0 || max <= min)
    stop("Invalid min/max values")
  min + sample(max - min, 1) - 1
}


# A scope where we can put mutable global state
.globals <- new.env(parent = emptyenv())
.globals$ownSeed <- NULL

withPrivateSeed <-function (expr) {
  if (exists(".Random.seed", envir = .GlobalEnv, inherits = FALSE)) {
    hasOrigSeed <- TRUE
    origSeed <- .GlobalEnv$.Random.seed
  }
  else {
    hasOrigSeed <- FALSE
  }
  if (is.null(.globals$ownSeed)) {
    if (hasOrigSeed) {
      rm(.Random.seed, envir = .GlobalEnv, inherits = FALSE)
    }
  }
  else {
    .GlobalEnv$.Random.seed <- .globals$ownSeed
  }
  on.exit({
    .globals$ownSeed <- .GlobalEnv$.Random.seed
    if (hasOrigSeed) {
      .GlobalEnv$.Random.seed <- origSeed
    } else {
      rm(.Random.seed, envir = .GlobalEnv, inherits = FALSE)
    }
    httpuv::getRNGState()
  })
  expr
}


p_randomInt <- function (...) {
  withPrivateSeed(randomInt(...))
}


markTabAsSelected <- function (x) {
  attr(x, "selected") <- TRUE
  x
}


`%OR%` <- function (x, y)
{
  if (is.null(x) || isTRUE(is.na(x)))
    y
  else x
}


findAndMarkSelectedTab <- function (tabs, selected, foundSelected) {
  tabs <- lapply(tabs, function(div) {
    if (foundSelected || is.character(div)) {
    }
    else if (inherits(div, "shiny.navbarmenu")) {
      res <- findAndMarkSelectedTab(div$tabs, selected,
                                    foundSelected)
      div$tabs <- res$tabs
      foundSelected <<- res$foundSelected
    }
    else {
      if (is.null(selected)) {
        foundSelected <<- TRUE
        div <- markTabAsSelected(div)
      }
      else {
        tabValue <- div$attribs$`data-value` %OR% div$attribs$title
        if (identical(selected, tabValue)) {
          foundSelected <<- TRUE
          div <- markTabAsSelected(div)
        }
      }
    }
    return(div)
  })
  return(list(tabs = tabs, foundSelected = foundSelected))
}



anyNamed <- function (x)
{
  if (length(x) == 0)
    return(FALSE)
  nms <- names(x)
  if (is.null(nms))
    return(FALSE)
  any(nzchar(nms))
}



buildTabset <- function (tabs, ulClass, textFilter = NULL, id = NULL, selected = NULL,
                         foundSelected = FALSE) {
  res <- findAndMarkSelectedTab(tabs, selected, foundSelected)
  tabs <- res$tabs
  foundSelected <- res$foundSelected
  if (!is.null(id))
    ulClass <- paste(ulClass, "shiny-tab-input")
  if (anyNamed(tabs)) {
    nms <- names(tabs)
    nms <- nms[nzchar(nms)]
    stop("Tabs should all be unnamed arguments, but some are named: ",
         paste(nms, collapse = ", "))
  }
  tabsetId <- p_randomInt(1000, 10000)
  tabs <- lapply(seq_len(length(tabs)), buildTabItem, tabsetId = tabsetId,
                 foundSelected = foundSelected, tabs = tabs, textFilter = textFilter)
  tabNavList <- shiny::tags$ul(class = ulClass, id = id, `data-tabsetid` = tabsetId,
                        lapply(tabs, "[[", 1))
  tabContent <- shiny::tags$div(class = "tab-content", `data-tabsetid` = tabsetId,
                         lapply(tabs, "[[", 2))
  list(navList = tabNavList, content = tabContent)
}


isTabSelected <- function (x) {
  isTRUE(attr(x, "selected", exact = TRUE))
}


containsSelectedTab <- function (tabs) {
  any(vapply(tabs, isTabSelected, logical(1)))
}


getIcon <- function (tab = NULL, iconClass = NULL) {
  if (!is.null(tab))
    iconClass <- tab$attribs$`data-icon-class`
  if (!is.null(iconClass)) {
    if (grepl("fa-", iconClass, fixed = TRUE)) {
      iconClass <- paste(iconClass, "fa-fw")
    }
    shiny::icon(name = NULL, class = iconClass)
  }
  else NULL
}


navbarMenuTextFilter <- function (text) {
  if (grepl("^\\-+$", text))
    shiny::tags$li(class = "divider")
  else shiny::tags$li(class = "dropdown-header", text)
}


buildTabItem <- function (index, tabsetId, foundSelected, tabs = NULL, divTag = NULL,
                          textFilter = NULL) {
  divTag <- if (!is.null(divTag))
    divTag
  else tabs[[index]]
  if (is.character(divTag) && !is.null(textFilter)) {
    liTag <- textFilter(divTag)
    divTag <- NULL
  }
  else if (inherits(divTag, "shiny.navbarmenu")) {
    tabset <- buildTabset(divTag$tabs, "dropdown-menu", navbarMenuTextFilter,
                          foundSelected = foundSelected)
    containsSelected <- containsSelectedTab(divTag$tabs)
    liTag <- shiny::tags$li(class = paste0("dropdown", if (containsSelected)
      " active"), shiny::tags$a(href = "#", class = "dropdown-toggle",
                         `data-toggle` = "dropdown", `data-value` = divTag$menuName,
                         getIcon(iconClass = divTag$iconClass), divTag$title,
                         shiny::tags$b(class = "caret")), tabset$navList)
    divTag <- tabset$content$children
  }
  else {
    tabId <- paste("tab", tabsetId, index, sep = "-")
    liTag <- shiny::tags$li(shiny::tags$a(href = paste("#", tabId, sep = ""),
                            `data-toggle` = "tab", `data-value` = divTag$attribs$`data-value`,
                            getIcon(iconClass = divTag$attribs$`data-icon-class`),
                            divTag$attribs$title))
    if (isTabSelected(divTag)) {
      liTag$attribs$class <- "active"
      divTag$attribs$class <- "tab-pane active"
    }
    divTag$attribs$id <- tabId
    divTag$attribs$title <- NULL
  }
  return(list(liTag = liTag, divTag = divTag))
}



shinyDeprecated <- function (new = NULL, msg = NULL, old = as.character(sys.call(sys.parent()))[1L], 
          version = NULL) 
{
  if (getOption("shiny.deprecation.messages") %OR% TRUE == 
      FALSE) 
    return(invisible())
  if (is.null(msg)) {
    msg <- paste(old, "is deprecated.")
    if (!is.null(new)) {
      msg <- paste(msg, "Please use", new, "instead.", 
                   "To disable this message, run options(shiny.deprecation.messages=FALSE)")
    }
  }
  if (!is.null(version)) {
    msg <- paste0(msg, " (Last used in version ", version, 
                  ")")
  }
  message(msg)
}



bs3_tabsetPanel <- function (tabs, id = NULL, selected = NULL, 
                             type = c("tabs", "pills", "hidden")) 
{
  if (!is.null(id)) 
    selected <- shiny::restoreInput(id = id, default = selected)
  type <- match.arg(type)
  tabset <- buildTabset(tabs, paste0("nav nav-", type), NULL, id, selected)
  first <- tabset$navList
  second <- tabset$content
  shiny::tags$div(class = "tabbable", first, second)
}




validateIcon <- function (icon) 
{
  if (is.null(icon) || identical(icon, character(0))) {
    return(icon)
  }
  else if (inherits(icon, "shiny.tag") && icon$name == "i") {
    return(icon)
  }
  else {
    stop("Invalid icon. Use Shiny's 'icon()' function to generate a valid icon")
  }
}




waiterShowOnLoad <- function(
  html = waiter::spin_1(), color = "#333e48"
){
  
  html <- as.character(html)
  html <- gsub("\n", "", html)
  
  show <- sprintf(
    "waiter.show({
      id: null,
      html: '%s', 
      color: '%s'
    });",
    html, color
  )
  
  shiny::HTML(sprintf("<script>%s</script>", show))
  
}


#' Create container for bs4Dash demo app
#' 
#' Container based on device.css
#'
#' @param url app URL. httr GET test is run before. If failed,
#' function returns NULL.
#' @param deps Whether to include marvel device assets. Default to FALSE.
#' The first occurence must set deps to TRUE so that CSS is loaded in the page.
#' @keywords internal
app_container <- function(url, deps = FALSE) {
  
  # test app availability
  req <- httr::GET(url)
  show_app <- req$status_code == 200
  
  if (show_app) {
    device_tag <- shiny::div(
      class="marvel-device ipad black",
      shiny::div(class = "camera"),
      shiny::div(
        class = "screen",
        shiny::tags$iframe(
          width = "100%",
          src = url,
          allowfullscreen = "",
          frameborder = "0",
          scrolling = "yes",
          height = "770px"
        )
      ),
      shiny::div(class = "home")
    )
    if (deps){
      shiny::tagList(
        shiny::tags$link(
          rel = "stylesheet",
          href = system.file("marvel-devices-css-1.0.0/devices.min.css", package = "bs4Dash"),
          type = "text/css"
        ),
        device_tag
      )
    } else {
      device_tag
    }
  } 
}

# Get parent function arguments
get_parent_args <- function() {
  cl <- sys.call(-3)
  as.list(cl)[-1]
}
DivadNojnarg/bs4Dash documentation built on Feb. 5, 2024, 9:02 a.m.