diff --git a/NAMESPACE b/NAMESPACE
index 8f9f975728..16a033c573 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -408,6 +408,7 @@ importFrom(rlang,is_false)
importFrom(rlang,is_missing)
importFrom(rlang,is_na)
importFrom(rlang,is_quosure)
+importFrom(rlang,list2)
importFrom(rlang,maybe_missing)
importFrom(rlang,missing_arg)
importFrom(rlang,new_function)
diff --git a/R/bootstrap.R b/R/bootstrap.R
index 66d534d7bb..087b863cb1 100644
--- a/R/bootstrap.R
+++ b/R/bootstrap.R
@@ -163,6 +163,15 @@ getCurrentTheme <- function() {
getShinyOption("bootstrapTheme", default = NULL)
}
+getCurrentVersion <- function() {
+ theme <- getCurrentTheme()
+ if (bslib::is_bs_theme(theme)) {
+ bslib::theme_version(theme)
+ } else {
+ strsplit(bootstrapVersion, ".", fixed = TRUE)[[1]][[1]]
+ }
+}
+
setCurrentTheme <- function(theme) {
shinyOptions(bootstrapTheme = theme)
}
@@ -211,7 +220,7 @@ registerThemeDependency <- function(func) {
bootstrapDependency <- function(theme) {
htmlDependency(
- "bootstrap", "3.4.1",
+ "bootstrap", bootstrapVersion,
c(
href = "shared/bootstrap",
file = system.file("www/shared/bootstrap", package = "shiny")
@@ -230,6 +239,8 @@ bootstrapDependency <- function(theme) {
)
}
+bootstrapVersion <- "3.4.1"
+
#' @rdname bootstrapPage
#' @export
@@ -436,10 +447,11 @@ navbarPage <- function(title,
pageTitle <- title
# navbar class based on options
+ # TODO: tagFunction() the navbar logic?
navbarClass <- "navbar navbar-default"
position <- match.arg(position)
if (!is.null(position))
- navbarClass <- paste(navbarClass, " navbar-", position, sep = "")
+ navbarClass <- paste0(navbarClass, " navbar-", position)
if (inverse)
navbarClass <- paste(navbarClass, "navbar-inverse")
@@ -447,21 +459,14 @@ navbarPage <- function(title,
selected <- restoreInput(id = id, default = selected)
# build the tabset
- tabs <- list(...)
- tabset <- buildTabset(tabs, "nav navbar-nav", NULL, id, selected)
-
- # function to return plain or fluid class name
- className <- function(name) {
- if (fluid)
- paste(name, "-fluid", sep="")
- else
- name
- }
+ tabset <- buildTabset(..., ulClass = "nav navbar-nav", id = id, selected = selected)
+
+ containerClass <- paste0("container", if (fluid) "-fluid")
# built the container div dynamically to support optional collapsibility
if (collapsible) {
- navId <- paste("navbar-collapse-", p_randomInt(1000, 10000), sep="")
- containerDiv <- div(class=className("container"),
+ navId <- paste0("navbar-collapse-", p_randomInt(1000, 10000))
+ containerDiv <- div(class=containerClass,
div(class="navbar-header",
tags$button(type="button", class="navbar-toggle collapsed",
`data-toggle`="collapse", `data-target`=paste0("#", navId),
@@ -475,7 +480,7 @@ navbarPage <- function(title,
div(class="navbar-collapse collapse", id=navId, tabset$navList)
)
} else {
- containerDiv <- div(class=className("container"),
+ containerDiv <- div(class=containerClass,
div(class="navbar-header",
span(class="navbar-brand", pageTitle)
),
@@ -484,7 +489,7 @@ navbarPage <- function(title,
}
# build the main tab content div
- contentDiv <- div(class=className("container"))
+ contentDiv <- div(class=containerClass)
if (!is.null(header))
contentDiv <- tagAppendChild(contentDiv, div(class="row", header))
contentDiv <- tagAppendChild(contentDiv, tabset$content)
@@ -511,11 +516,15 @@ navbarPage <- function(title,
navbarMenu <- function(title, ..., menuName = title, icon = NULL) {
structure(list(title = title,
menuName = menuName,
- tabs = list(...),
+ tabs = list2(...),
iconClass = iconClass(icon)),
class = "shiny.navbarmenu")
}
+isNavbarMenu <- function(x) {
+ inherits(x, "shiny.navbarmenu")
+}
+
#' Create a well panel
#'
#' Creates a panel with a slightly inset border and grey background. Equivalent
@@ -656,6 +665,13 @@ tabPanel <- function(title, ..., value = title, icon = NULL) {
...
)
}
+
+isTabPanel <- function(x) {
+ if (!inherits(x, "shiny.tag")) return(FALSE)
+ class <- tagGetAttribute(x, "class") %||% ""
+ "tab-pane" %in% strsplit(class, "\\s+")[[1]]
+}
+
#' @export
#' @describeIn tabPanel Create a tab panel that drops the title argument.
#' This function should be used within `tabsetPanel(type = "hidden")`. See [tabsetPanel()] for example usage.
@@ -693,6 +709,7 @@ tabPanelBody <- function(value, ..., icon = NULL) {
#' }
#' @param position This argument is deprecated; it has been discontinued in
#' Bootstrap 3.
+#' @inheritParams navbarPage
#' @return A tabset that can be passed to [mainPanel()]
#'
#' @seealso [tabPanel()], [updateTabsetPanel()],
@@ -742,6 +759,8 @@ tabsetPanel <- function(...,
id = NULL,
selected = NULL,
type = c("tabs", "pills", "hidden"),
+ header = NULL,
+ footer = NULL,
position = deprecated()) {
if (lifecycle::is_present(position)) {
shinyDeprecated(
@@ -753,18 +772,18 @@ tabsetPanel <- function(...,
if (!is.null(id))
selected <- restoreInput(id = id, default = selected)
- # build the tabset
- tabs <- list(...)
type <- match.arg(type)
-
- tabset <- buildTabset(tabs, paste0("nav nav-", type), NULL, id, selected)
-
- # create the content
- first <- tabset$navList
- second <- tabset$content
-
- # create the tab div
- tags$div(class = "tabbable", first, second)
+ tabset <- buildTabset(..., ulClass = paste0("nav nav-", type), id = id, selected = selected)
+
+ tags$div(
+ class = "tabbable",
+ !!!dropNulls(list(
+ tabset$navList,
+ header,
+ tabset$content,
+ footer
+ ))
+ )
}
#' Create a navigation list panel
@@ -784,8 +803,10 @@ tabsetPanel <- function(...,
#' navigation list.
#' @param fluid `TRUE` to use fluid layout; `FALSE` to use fixed
#' layout.
-#' @param widths Column withs of the navigation list and tabset content areas
+#' @param widths Column widths of the navigation list and tabset content areas
#' respectively.
+#' @inheritParams tabsetPanel
+#' @inheritParams navbarPage
#'
#' @details You can include headers within the `navlistPanel` by including
#' plain text elements in the list. Versions of Shiny before 0.11 supported
@@ -812,37 +833,30 @@ tabsetPanel <- function(...,
navlistPanel <- function(...,
id = NULL,
selected = NULL,
+ header = NULL,
+ footer = NULL,
well = TRUE,
fluid = TRUE,
widths = c(4, 8)) {
- # text filter for headers
- textFilter <- function(text) {
- tags$li(class="navbar-brand", text)
- }
-
if (!is.null(id))
selected <- restoreInput(id = id, default = selected)
- # build the tabset
- tabs <- list(...)
- tabset <- buildTabset(tabs,
- "nav nav-pills nav-stacked",
- textFilter,
- id,
- selected)
-
- # create the columns
- columns <- list(
- column(widths[[1]], class=ifelse(well, "well", ""), tabset$navList),
- column(widths[[2]], tabset$content)
+ tabset <- buildTabset(
+ ..., ulClass = "nav nav-pills nav-stacked",
+ textFilter = function(text) tags$li(class = "navbar-brand", text),
+ id = id, selected = selected
)
- # return the row
- if (fluid)
- fluidRow(columns)
- else
- fixedRow(columns)
+ row <- if (fluid) fluidRow else fixedRow
+
+ row(
+ column(widths[[1]], class = if (well) "well", tabset$navList),
+ column(
+ widths[[2]],
+ !!!dropNulls(list(header, tabset$content, footer))
+ )
+ )
}
# Helpers to build tabsetPanels (& Co.) and their elements
@@ -860,14 +874,14 @@ containsSelectedTab <- function(tabs) {
}
findAndMarkSelectedTab <- function(tabs, selected, foundSelected) {
- tabs <- lapply(tabs, function(div) {
- if (foundSelected || is.character(div)) {
+ tabs <- lapply(tabs, function(x) {
+ if (foundSelected || is.character(x)) {
# Strings are not selectable items
- } else if (inherits(div, "shiny.navbarmenu")) {
+ } else if (isNavbarMenu(x)) {
# Recur for navbarMenus
- res <- findAndMarkSelectedTab(div$tabs, selected, foundSelected)
- div$tabs <- res$tabs
+ res <- findAndMarkSelectedTab(x$tabs, selected, foundSelected)
+ x$tabs <- res$tabs
foundSelected <<- res$foundSelected
} else {
@@ -876,16 +890,16 @@ findAndMarkSelectedTab <- function(tabs, selected, foundSelected) {
# mark first available item as selected
if (is.null(selected)) {
foundSelected <<- TRUE
- div <- markTabAsSelected(div)
+ x <- markTabAsSelected(x)
} else {
- tabValue <- div$attribs$`data-value` %||% div$attribs$title
+ tabValue <- x$attribs$`data-value` %||% x$attribs$title
if (identical(selected, tabValue)) {
foundSelected <<- TRUE
- div <- markTabAsSelected(div)
+ x <- markTabAsSelected(x)
}
}
}
- return(div)
+ return(x)
})
return(list(tabs = tabs, foundSelected = foundSelected))
}
@@ -911,9 +925,10 @@ navbarMenuTextFilter <- function(text) {
# This function is called internally by navbarPage, tabsetPanel
# and navlistPanel
-buildTabset <- function(tabs, ulClass, textFilter = NULL, id = NULL,
+buildTabset <- function(..., ulClass, textFilter = NULL, id = NULL,
selected = NULL, foundSelected = FALSE) {
+ tabs <- dropNulls(list2(...))
res <- findAndMarkSelectedTab(tabs, selected, foundSelected)
tabs <- res$tabs
foundSelected <- res$foundSelected
@@ -934,10 +949,10 @@ buildTabset <- function(tabs, ulClass, textFilter = NULL, id = NULL,
tabs = tabs, textFilter = textFilter)
tabNavList <- tags$ul(class = ulClass, id = id,
- `data-tabsetid` = tabsetId, lapply(tabs, "[[", 1))
+ `data-tabsetid` = tabsetId, !!!lapply(tabs, "[[", "liTag"))
tabContent <- tags$div(class = "tab-content",
- `data-tabsetid` = tabsetId, lapply(tabs, "[[", 2))
+ `data-tabsetid` = tabsetId, !!!lapply(tabs, "[[", "divTag"))
list(navList = tabNavList, content = tabContent)
}
@@ -949,56 +964,173 @@ buildTabset <- function(tabs, ulClass, textFilter = NULL, id = NULL,
buildTabItem <- function(index, tabsetId, foundSelected, tabs = NULL,
divTag = NULL, textFilter = NULL) {
- divTag <- if (!is.null(divTag)) divTag else tabs[[index]]
+ divTag <- divTag %||% tabs[[index]]
+ # Handles navlistPanel() headers and dropdown dividers
if (is.character(divTag) && !is.null(textFilter)) {
- # text item: pass it to the textFilter if it exists
- liTag <- textFilter(divTag)
- divTag <- NULL
-
- } else if (inherits(divTag, "shiny.navbarmenu")) {
- # navbarMenu item: build the child tabset
- tabset <- buildTabset(divTag$tabs, "dropdown-menu",
- navbarMenuTextFilter, foundSelected = foundSelected)
-
- # if this navbarMenu contains a selected item, mark it active
- containsSelected <- containsSelectedTab(divTag$tabs)
- liTag <- tags$li(
- class = paste0("dropdown", if (containsSelected) " active"),
- tags$a(href = "#",
- class = "dropdown-toggle", `data-toggle` = "dropdown",
- `data-value` = divTag$menuName,
- getIcon(iconClass = divTag$iconClass),
- divTag$title, tags$b(class = "caret")
- ),
- tabset$navList # inner tabPanels items
+ return(list(liTag = textFilter(divTag), divTag = NULL))
+ }
+
+ if (isNavbarMenu(divTag)) {
+ # tabPanelMenu item: build the child tabset
+ tabset <- buildTabset(
+ !!!divTag$tabs, ulClass = "dropdown-menu",
+ textFilter = navbarMenuTextFilter,
+ foundSelected = foundSelected
)
+ return(buildDropdown(divTag, tabset))
+ }
+
+ if (isTabPanel(divTag)) {
+ return(buildNavItem(divTag, tabsetId, index))
+ }
+
+ # The behavior is undefined at this point, so construct a condition message
+ msg <- paste0(
+ "Expected a collection `tabPanel()`s",
+ if (is.null(textFilter)) " and `navbarMenu()`.",
+ if (!is.null(textFilter)) ", `navbarMenu()`, and/or character strings.",
+ " Consider using `header` or `footer` if you wish to place content above (or below) every panel's contents"
+ )
+
+ # Luckily this case has never worked, so it's safe to throw here
+ # https://github.com/rstudio/shiny/issues/3313
+ if (!inherits(divTag, "shiny.tag")) {
+ stop(msg, call. = FALSE)
+ }
+
+ # Unfortunately, this 'off-label' use case creates an 'empty' nav and includes
+ # the divTag content on every tab. There shouldn't be any reason to be relying on
+ # this behavior since we now have pre/post arguments, so throw a warning, but still
+ # support the use case since we don't make breaking changes
+ warning(msg, call. = FALSE)
+
+ return(buildNavItem(divTag, tabsetId, index))
+}
+
+buildNavItem <- function(divTag, tabsetId, index) {
+ id <- paste("tab", tabsetId, index, sep = "-")
+ title <- tagGetAttribute(divTag, "title")
+ value <- tagGetAttribute(divTag, "data-value")
+ icon <- getIcon(iconClass = tagGetAttribute(divTag, "data-icon-class"))
+ active <- isTabSelected(divTag)
+ divTag <- tagAppendAttributes(divTag, class = if (active) "active")
+ divTag$attribs$id <- id
+ divTag$attribs$title <- NULL
+ list(
+ divTag = divTag,
+ liTag = tagFunction(function() {
+ navItem <- if ("3" %in% getCurrentVersion()) bs3NavItem else bs4NavItem
+ navItem(id, title, value, icon, active)
+ })
+ )
+}
+
+buildDropdown <- function(divTag, tabset) {
+ title <- divTag$title
+ value <- divTag$menuName
+ icon <- getIcon(iconClass = divTag$iconClass)
+ active <- containsSelectedTab(divTag$tabs)
+ list(
# list of tab content divs from the child tabset
- divTag <- tabset$content$children
+ divTag = tabset$content$children,
+ liTag = tagFunction(function() {
+ if ("3" %in% getCurrentVersion()) {
+ bs3NavItemDropdown(title, value, icon, active, tabset$navList)
+ } else {
+ # In BS4, dropdown nav anchors can't be wrapped in a
tag
+ # and also need .nav-link replaced with .dropdown-item to be
+ # styled sensibly
+ items <- tabset$navList
+ items$children <- lapply(items$children, function(x) {
+ # x should be a tagFunction() due to the else block below
+ x <- if (inherits(x, "shiny.tag.function")) x() else x
+ # Replace
\");\n var $content = $notification.find(\".shiny-notification-content\");\n Shiny.renderContent($content, {\n html: newHtml,\n deps: deps\n }); // Remove any existing classes of the form 'shiny-notification-xxxx'.\n // The xxxx would be strings like 'warning'.\n\n var classes = $notification.attr(\"class\").split(/\\s+/).filter(function (cls) {\n return cls.match(/^shiny-notification-/);\n }).join(\" \");\n $notification.removeClass(classes); // Add class. 'default' means no additional CSS class.\n\n if (type && type !== \"default\") $notification.addClass(\"shiny-notification-\" + type); // Make sure that the presence/absence of close button matches with value\n // of `closeButton`.\n\n var $close = $notification.find(\".shiny-notification-close\");\n\n if (closeButton && $close.length === 0) {\n $notification.append('
×
');\n } else if (!closeButton && $close.length !== 0) {\n $close.remove();\n } // If duration was provided, schedule removal. If not, clear existing\n // removal callback (this happens if a message was first added with\n // a duration, and then updated with no duration).\n\n\n if (duration) _addRemovalCallback(id, duration);else _clearRemovalCallback(id);\n return id;\n }\n\n function remove(id) {\n _get(id).fadeOut(fadeDuration, function () {\n Shiny.unbindAll(this);\n $(this).remove(); // If no more notifications, remove the panel from the DOM.\n\n if (_ids().length === 0) {\n _getPanel().remove();\n }\n });\n } // Returns an individual notification DOM object (wrapped in jQuery).\n\n\n function _get(id) {\n if (!id) return null;\n return _getPanel().find(\"#shiny-notification-\" + $escape(id));\n } // Return array of all notification IDs\n\n\n function _ids() {\n return _getPanel().find(\".shiny-notification\").map(function () {\n return this.id.replace(/shiny-notification-/, \"\");\n }).get();\n } // Returns the notification panel DOM object (wrapped in jQuery).\n\n\n function _getPanel() {\n return $(\"#shiny-notification-panel\");\n } // Create notifications panel and return the jQuery object. If the DOM\n // element already exists, just return it.\n\n\n function _createPanel() {\n var $panel = _getPanel();\n\n if ($panel.length > 0) return $panel;\n $(document.body).append('
');\n return $panel;\n } // Create a notification DOM element and return the jQuery object. If the\n // DOM element already exists for the ID, just return it without creating.\n\n\n function _create(id) {\n var $notification = _get(id);\n\n if ($notification.length === 0) {\n $notification = $(\"
\") + '
×
' + '' + \"
\");\n $notification.find(\".shiny-notification-close\").on(\"click\", function (e) {\n e.preventDefault();\n e.stopPropagation();\n remove(id);\n });\n\n _getPanel().append($notification);\n }\n\n return $notification;\n } // Add a callback to remove a notification after a delay in ms.\n\n\n function _addRemovalCallback(id, delay) {\n // If there's an existing removalCallback, clear it before adding the new\n // one.\n _clearRemovalCallback(id); // Attach new removal callback\n\n\n var removalCallback = setTimeout(function () {\n remove(id);\n }, delay);\n\n _get(id).data(\"removalCallback\", removalCallback);\n } // Clear a removal callback from a notification, if present.\n\n\n function _clearRemovalCallback(id) {\n var $notification = _get(id);\n\n var oldRemovalCallback = $notification.data(\"removalCallback\");\n\n if (oldRemovalCallback) {\n clearTimeout(oldRemovalCallback);\n }\n }\n\n return {\n show: show,\n remove: remove\n };\n }(); // \"modal.js\"\n\n\n Shiny.modal = {\n // Show a modal dialog. This is meant to handle two types of cases: one is\n // that the content is a Bootstrap modal dialog, and the other is that the\n // content is non-Bootstrap. Bootstrap modals require some special handling,\n // which is coded in here.\n show: function show() {\n var _ref4 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},\n _ref4$html = _ref4.html,\n html = _ref4$html === void 0 ? \"\" : _ref4$html,\n _ref4$deps = _ref4.deps,\n deps = _ref4$deps === void 0 ? [] : _ref4$deps;\n\n // If there was an existing Bootstrap modal, then there will be a modal-\n // backdrop div that was added outside of the modal wrapper, and it must be\n // removed; otherwise there can be multiple of these divs.\n $(\".modal-backdrop\").remove(); // Get existing wrapper DOM element, or create if needed.\n\n var $modal = $(\"#shiny-modal-wrapper\");\n\n if ($modal.length === 0) {\n $modal = $('');\n $(document.body).append($modal); // If the wrapper's content is a Bootstrap modal, then when the inner\n // modal is hidden, remove the entire thing, including wrapper.\n\n $modal.on(\"hidden.bs.modal\", function (e) {\n if (e.target === $(\"#shiny-modal\")[0]) {\n Shiny.unbindAll($modal);\n $modal.remove();\n }\n });\n }\n\n $modal.on(\"keydown.shinymodal\", function (e) {\n // If we're listening for Esc, don't let the event propagate. See\n // https://github.com/rstudio/shiny/issues/1453. The value of\n // data(\"keyboard\") needs to be checked inside the handler, because at\n // the time that $modal.on() is called, the $(\"#shiny-modal\") div doesn't\n // yet exist.\n if ($(\"#shiny-modal\").data(\"keyboard\") === false) return;\n\n if (e.keyCode === 27) {\n e.stopPropagation();\n e.preventDefault();\n }\n }); // Set/replace contents of wrapper with html.\n\n Shiny.renderContent($modal, {\n html: html,\n deps: deps\n });\n },\n remove: function remove() {\n var $modal = $(\"#shiny-modal-wrapper\");\n $modal.off(\"keydown.shinymodal\"); // Look for a Bootstrap modal and if present, trigger hide event. This will\n // trigger the hidden.bs.modal callback that we set in show(), which unbinds\n // and removes the element.\n\n if ($modal.find(\".modal\").length > 0) {\n $modal.find(\".modal\").modal(\"hide\");\n } else {\n // If not a Bootstrap modal dialog, simply unbind and remove it.\n Shiny.unbindAll($modal);\n $modal.remove();\n }\n }\n }; // \"file_processor.js\"\n // \u221A\n // \"binding_registry.js\"\n\n var BindingRegistry = function BindingRegistry() {\n this.bindings = [];\n this.bindingNames = {};\n };\n\n (function () {\n this.register = function (binding, bindingName, priority) {\n var bindingObj = {\n binding: binding,\n priority: priority || 0\n };\n this.bindings.unshift(bindingObj);\n\n if (bindingName) {\n this.bindingNames[bindingName] = bindingObj;\n binding.name = bindingName;\n }\n };\n\n this.setPriority = function (bindingName, priority) {\n var bindingObj = this.bindingNames[bindingName];\n if (!bindingObj) throw \"Tried to set priority on unknown binding \" + bindingName;\n bindingObj.priority = priority || 0;\n };\n\n this.getPriority = function (bindingName) {\n var bindingObj = this.bindingNames[bindingName];\n if (!bindingObj) return false;\n return bindingObj.priority;\n };\n\n this.getBindings = function () {\n // Sort the bindings. The ones with higher priority are consulted\n // first; ties are broken by most-recently-registered.\n return mergeSort(this.bindings, function (a, b) {\n return b.priority - a.priority;\n });\n };\n }).call(BindingRegistry.prototype);\n var inputBindings = Shiny.inputBindings = new BindingRegistry();\n var outputBindings = Shiny.outputBindings = new BindingRegistry(); // \"output_binding.js\"\n\n var OutputBinding = Shiny.OutputBinding = function () {};\n\n (function () {\n // Returns a jQuery object or element array that contains the\n // descendants of scope that match this binding\n this.find = function (scope) {\n throw \"Not implemented\";\n };\n\n this.getId = function (el) {\n return el[\"data-input-id\"] || el.id;\n };\n\n this.onValueChange = function (el, data) {\n this.clearError(el);\n this.renderValue(el, data);\n };\n\n this.onValueError = function (el, err) {\n this.renderError(el, err);\n };\n\n this.renderError = function (el, err) {\n this.clearError(el);\n\n if (err.message === \"\") {\n // not really error, but we just need to wait (e.g. action buttons)\n $(el).empty();\n return;\n }\n\n var errClass = \"shiny-output-error\";\n\n if (err.type !== null) {\n // use the classes of the error condition as CSS class names\n errClass = errClass + \" \" + $.map(asArray(err.type), function (type) {\n return errClass + \"-\" + type;\n }).join(\" \");\n }\n\n $(el).addClass(errClass).text(err.message);\n };\n\n this.clearError = function (el) {\n $(el).attr(\"class\", function (i, c) {\n return c.replace(/(^|\\s)shiny-output-error\\S*/g, \"\");\n });\n };\n\n this.showProgress = function (el, show) {\n var RECALC_CLASS = \"recalculating\";\n if (show) $(el).addClass(RECALC_CLASS);else $(el).removeClass(RECALC_CLASS);\n };\n }).call(OutputBinding.prototype); // \"output_binding_text.js\"\n\n var textOutputBinding = new OutputBinding();\n $.extend(textOutputBinding, {\n find: function find(scope) {\n return $(scope).find(\".shiny-text-output\");\n },\n renderValue: function renderValue(el, data) {\n $(el).text(data);\n }\n });\n outputBindings.register(textOutputBinding, \"shiny.textOutput\"); // \"output_binding_image.js\"\n\n var imageOutputBinding = new OutputBinding();\n $.extend(imageOutputBinding, {\n find: function find(scope) {\n return $(scope).find(\".shiny-image-output, .shiny-plot-output\");\n },\n renderValue: function renderValue(el, data) {\n // The overall strategy:\n // * Clear out existing image and event handlers.\n // * Create new image.\n // * Create various event handlers.\n // * Bind those event handlers to events.\n // * Insert the new image.\n var outputId = this.getId(el);\n var $el = $(el);\n var img; // Get existing img element if present.\n\n var $img = $el.find(\"img\");\n\n if ($img.length === 0) {\n // If a img element is not already present, that means this is either\n // the first time renderValue() has been called, or this is after an\n // error.\n img = document.createElement(\"img\");\n $el.append(img);\n $img = $(img);\n } else {\n // Trigger custom 'reset' event for any existing images in the div\n img = $img[0];\n $img.trigger(\"reset\");\n }\n\n if (!data) {\n $el.empty();\n return;\n } // If value is undefined, return alternate. Sort of like ||, except it won't\n // return alternate for other falsy values (0, false, null).\n\n\n function OR(value, alternate) {\n if (value === undefined) return alternate;\n return value;\n }\n\n var opts = {\n clickId: $el.data(\"click-id\"),\n clickClip: OR(strToBool($el.data(\"click-clip\")), true),\n dblclickId: $el.data(\"dblclick-id\"),\n dblclickClip: OR(strToBool($el.data(\"dblclick-clip\")), true),\n dblclickDelay: OR($el.data(\"dblclick-delay\"), 400),\n hoverId: $el.data(\"hover-id\"),\n hoverClip: OR(strToBool($el.data(\"hover-clip\")), true),\n hoverDelayType: OR($el.data(\"hover-delay-type\"), \"debounce\"),\n hoverDelay: OR($el.data(\"hover-delay\"), 300),\n hoverNullOutside: OR(strToBool($el.data(\"hover-null-outside\")), false),\n brushId: $el.data(\"brush-id\"),\n brushClip: OR(strToBool($el.data(\"brush-clip\")), true),\n brushDelayType: OR($el.data(\"brush-delay-type\"), \"debounce\"),\n brushDelay: OR($el.data(\"brush-delay\"), 300),\n brushFill: OR($el.data(\"brush-fill\"), \"#666\"),\n brushStroke: OR($el.data(\"brush-stroke\"), \"#000\"),\n brushOpacity: OR($el.data(\"brush-opacity\"), 0.3),\n brushDirection: OR($el.data(\"brush-direction\"), \"xy\"),\n brushResetOnNew: OR(strToBool($el.data(\"brush-reset-on-new\")), false),\n coordmap: data.coordmap\n };\n\n if (opts.brushFill === \"auto\") {\n opts.brushFill = getComputedLinkColor($el[0]);\n }\n\n if (opts.brushStroke === \"auto\") {\n opts.brushStroke = getStyle($el[0], \"color\");\n } // Copy items from data to img. Don't set the coordmap as an attribute.\n\n\n $.each(data, function (key, value) {\n if (value === null || key === \"coordmap\") {\n return;\n } // this checks only against base64 encoded src values\n // images put here are only from renderImage and renderPlot\n\n\n if (key === \"src\" && value === img.getAttribute(\"src\")) {\n // Ensure the browser actually fires an onLoad event, which doesn't\n // happen on WebKit if the value we set on src is the same as the\n // value it already has\n // https://github.com/rstudio/shiny/issues/2197\n // https://stackoverflow.com/questions/5024111/javascript-image-onload-doesnt-fire-in-webkit-if-loading-same-image\n img.removeAttribute(\"src\");\n }\n\n img.setAttribute(key, value);\n }); // Unset any attributes in the current img that were not provided in the\n // new data.\n\n for (var i = 0; i < img.attributes.length; i++) {\n var attrib = img.attributes[i]; // Need to check attrib.specified on IE because img.attributes contains\n // all possible attributes on IE.\n\n if (attrib.specified && !data.hasOwnProperty(attrib.name)) {\n img.removeAttribute(attrib.name);\n }\n }\n\n if (!opts.coordmap) {\n opts.coordmap = {\n panels: [],\n dims: {\n // These values be set to the naturalWidth and naturalHeight once the image has loaded\n height: null,\n width: null\n }\n };\n } // Remove event handlers that were added in previous runs of this function.\n\n\n $el.off(\".image_output\");\n $img.off(\".image_output\"); // When the image loads, initialize all the interaction handlers. When the\n // value of src is set, the browser may not load the image immediately,\n // even if it's a data URL. If we try to initialize this stuff\n // immediately, it can cause problems because we use we need the raw image\n // height and width\n\n $img.off(\"load.shiny_image_interaction\");\n $img.one(\"load.shiny_image_interaction\", function () {\n imageutils.initCoordmap($el, opts.coordmap); // This object listens for mousedowns, and triggers mousedown2 and dblclick2\n // events as appropriate.\n\n var clickInfo = imageutils.createClickInfo($el, opts.dblclickId, opts.dblclickDelay);\n $el.on(\"mousedown.image_output\", clickInfo.mousedown);\n\n if (isIE() && IEVersion() === 8) {\n $el.on(\"dblclick.image_output\", clickInfo.dblclickIE8);\n } // ----------------------------------------------------------\n // Register the various event handlers\n // ----------------------------------------------------------\n\n\n if (opts.clickId) {\n imageutils.disableDrag($el, $img);\n var clickHandler = imageutils.createClickHandler(opts.clickId, opts.clickClip, opts.coordmap);\n $el.on(\"mousedown2.image_output\", clickHandler.mousedown);\n $el.on(\"resize.image_output\", clickHandler.onResize); // When img is reset, do housekeeping: clear $el's mouse listener and\n // call the handler's onResetImg callback.\n\n $img.on(\"reset.image_output\", clickHandler.onResetImg);\n }\n\n if (opts.dblclickId) {\n imageutils.disableDrag($el, $img); // We'll use the clickHandler's mousedown function, but register it to\n // our custom 'dblclick2' event.\n\n var dblclickHandler = imageutils.createClickHandler(opts.dblclickId, opts.clickClip, opts.coordmap);\n $el.on(\"dblclick2.image_output\", dblclickHandler.mousedown);\n $el.on(\"resize.image_output\", dblclickHandler.onResize);\n $img.on(\"reset.image_output\", dblclickHandler.onResetImg);\n }\n\n if (opts.hoverId) {\n imageutils.disableDrag($el, $img);\n var hoverHandler = imageutils.createHoverHandler(opts.hoverId, opts.hoverDelay, opts.hoverDelayType, opts.hoverClip, opts.hoverNullOutside, opts.coordmap);\n $el.on(\"mousemove.image_output\", hoverHandler.mousemove);\n $el.on(\"mouseout.image_output\", hoverHandler.mouseout);\n $el.on(\"resize.image_output\", hoverHandler.onResize);\n $img.on(\"reset.image_output\", hoverHandler.onResetImg);\n }\n\n if (opts.brushId) {\n imageutils.disableDrag($el, $img);\n var brushHandler = imageutils.createBrushHandler(opts.brushId, $el, opts, opts.coordmap, outputId);\n $el.on(\"mousedown.image_output\", brushHandler.mousedown);\n $el.on(\"mousemove.image_output\", brushHandler.mousemove);\n $el.on(\"resize.image_output\", brushHandler.onResize);\n $img.on(\"reset.image_output\", brushHandler.onResetImg);\n }\n\n if (opts.clickId || opts.dblclickId || opts.hoverId || opts.brushId) {\n $el.addClass(\"crosshair\");\n }\n\n if (data.error) console.log(\"Error on server extracting coordmap: \" + data.error);\n });\n },\n renderError: function renderError(el, err) {\n $(el).find(\"img\").trigger(\"reset\");\n OutputBinding.prototype.renderError.call(this, el, err);\n },\n clearError: function clearError(el) {\n // Remove all elements except img and the brush; this is usually just\n // error messages.\n $(el).contents().filter(function () {\n return this.tagName !== \"IMG\" && this.id !== el.id + \"_brush\";\n }).remove();\n OutputBinding.prototype.clearError.call(this, el);\n },\n resize: function resize(el, width, height) {\n $(el).find(\"img\").trigger(\"resize\");\n }\n });\n outputBindings.register(imageOutputBinding, \"shiny.imageOutput\");\n var imageutils = {};\n\n imageutils.disableDrag = function ($el, $img) {\n // Make image non-draggable (Chrome, Safari)\n $img.css(\"-webkit-user-drag\", \"none\"); // Firefox, IE<=10\n // First remove existing handler so we don't keep adding handlers.\n\n $img.off(\"dragstart.image_output\");\n $img.on(\"dragstart.image_output\", function () {\n return false;\n }); // Disable selection of image and text when dragging in IE<=10\n\n $el.off(\"selectstart.image_output\");\n $el.on(\"selectstart.image_output\", function () {\n return false;\n });\n }; // Modifies the panel objects in a coordmap, adding scaleImgToData(),\n // scaleDataToImg(), and clipImg() functions to each one. The panel objects\n // use img and data coordinates only; they do not use css coordinates. The\n // domain is in data coordinates; the range is in img coordinates.\n\n\n imageutils.initPanelScales = function (panels) {\n // Map a value x from a domain to a range. If clip is true, clip it to the\n // range.\n function mapLinear(x, domainMin, domainMax, rangeMin, rangeMax, clip) {\n // By default, clip to range\n clip = clip || true;\n var factor = (rangeMax - rangeMin) / (domainMax - domainMin);\n var val = x - domainMin;\n var newval = val * factor + rangeMin;\n\n if (clip) {\n var max = Math.max(rangeMax, rangeMin);\n var min = Math.min(rangeMax, rangeMin);\n if (newval > max) newval = max;else if (newval < min) newval = min;\n }\n\n return newval;\n } // Create scale and inverse-scale functions for a single direction (x or y).\n\n\n function scaler1D(domainMin, domainMax, rangeMin, rangeMax, logbase) {\n return {\n scale: function scale(val, clip) {\n if (logbase) val = Math.log(val) / Math.log(logbase);\n return mapLinear(val, domainMin, domainMax, rangeMin, rangeMax, clip);\n },\n scaleInv: function scaleInv(val, clip) {\n var res = mapLinear(val, rangeMin, rangeMax, domainMin, domainMax, clip);\n if (logbase) res = Math.pow(logbase, res);\n return res;\n }\n };\n } // Modify panel, adding scale and inverse-scale functions that take objects\n // like {x:1, y:3}, and also add clip function.\n\n\n function addScaleFuns(panel) {\n var d = panel.domain;\n var r = panel.range;\n var xlog = panel.log && panel.log.x ? panel.log.x : null;\n var ylog = panel.log && panel.log.y ? panel.log.y : null;\n var xscaler = scaler1D(d.left, d.right, r.left, r.right, xlog);\n var yscaler = scaler1D(d.bottom, d.top, r.bottom, r.top, ylog); // Given an object of form {x:1, y:2}, or {x:1, xmin:2:, ymax: 3}, convert\n // from data coordinates to img. Whether a value is converted as x or y\n // depends on the first character of the key.\n\n panel.scaleDataToImg = function (val, clip) {\n return mapValues(val, function (value, key) {\n var prefix = key.substring(0, 1);\n\n if (prefix === \"x\") {\n return xscaler.scale(value, clip);\n } else if (prefix === \"y\") {\n return yscaler.scale(value, clip);\n }\n\n return null;\n });\n };\n\n panel.scaleImgToData = function (val, clip) {\n return mapValues(val, function (value, key) {\n var prefix = key.substring(0, 1);\n\n if (prefix === \"x\") {\n return xscaler.scaleInv(value, clip);\n } else if (prefix === \"y\") {\n return yscaler.scaleInv(value, clip);\n }\n\n return null;\n });\n }; // Given a scaled offset (in img pixels), clip it to the nearest panel region.\n\n\n panel.clipImg = function (offset_img) {\n var newOffset = {\n x: offset_img.x,\n y: offset_img.y\n };\n var bounds = panel.range;\n if (offset_img.x > bounds.right) newOffset.x = bounds.right;else if (offset_img.x < bounds.left) newOffset.x = bounds.left;\n if (offset_img.y > bounds.bottom) newOffset.y = bounds.bottom;else if (offset_img.y < bounds.top) newOffset.y = bounds.top;\n return newOffset;\n };\n } // Add the functions to each panel object.\n\n\n for (var i = 0; i < panels.length; i++) {\n var panel = panels[i];\n addScaleFuns(panel);\n }\n }; // This adds functions to the coordmap object to handle various\n // coordinate-mapping tasks, and send information to the server. The input\n // coordmap is an array of objects, each of which represents a panel. coordmap\n // must be an array, even if empty, so that it can be modified in place; when\n // empty, we add a dummy panel to the array. It also calls initPanelScales,\n // which modifies each panel object to have scaleImgToData, scaleDataToImg,\n // and clip functions.\n //\n // There are three coordinate spaces which we need to translate between:\n //\n // 1. css: The pixel coordinates in the web browser, also known as CSS pixels.\n // The origin is the upper-left corner of the (not including padding\n // and border).\n // 2. img: The pixel coordinates of the image data. A common case is on a\n // HiDPI device, where the source PNG image could be 1000 pixels wide but\n // be displayed in 500 CSS pixels. Another case is when the image has\n // additional scaling due to CSS transforms or width.\n // 3. data: The coordinates in the data space. This is a bit more complicated\n // than the other two, because there can be multiple panels (as in facets).\n\n\n imageutils.initCoordmap = function ($el, coordmap) {\n var $img = $el.find(\"img\");\n var img = $img[0]; // If we didn't get any panels, create a dummy one where the domain and range\n // are simply the pixel dimensions.\n // that we modify.\n\n if (coordmap.panels.length === 0) {\n var bounds = {\n top: 0,\n left: 0,\n right: img.clientWidth - 1,\n bottom: img.clientHeight - 1\n };\n coordmap.panels[0] = {\n domain: bounds,\n range: bounds,\n mapping: {}\n };\n } // If no dim height and width values are found, set them to the raw image height and width\n // These values should be the same...\n // This is only done to initialize an image output, whose height and width are unknown until the image is retrieved\n\n\n coordmap.dims.height = coordmap.dims.height || img.naturalHeight;\n coordmap.dims.width = coordmap.dims.width || img.naturalWidth; // Add scaling functions to each panel\n\n imageutils.initPanelScales(coordmap.panels); // This returns the offset of the mouse in CSS pixels relative to the img,\n // but not including the padding or border, if present.\n\n coordmap.mouseOffsetCss = function (mouseEvent) {\n var img_origin = findOrigin($img); // The offset of the mouse from the upper-left corner of the img, in\n // pixels.\n\n return {\n x: mouseEvent.pageX - img_origin.x,\n y: mouseEvent.pageY - img_origin.y\n };\n }; // Given an offset in an img in CSS pixels, return the corresponding offset\n // in source image pixels. The offset_css can have properties like \"x\",\n // \"xmin\", \"y\", and \"ymax\" -- anything that starts with \"x\" and \"y\". If the\n // img content is 1000 pixels wide, but is scaled to 400 pixels on screen,\n // and the input is x:400, then this will return x:1000.\n\n\n coordmap.scaleCssToImg = function (offset_css) {\n var pixel_scaling = coordmap.imgToCssScalingRatio();\n var result = mapValues(offset_css, function (value, key) {\n var prefix = key.substring(0, 1);\n\n if (prefix === \"x\") {\n return offset_css[key] / pixel_scaling.x;\n } else if (prefix === \"y\") {\n return offset_css[key] / pixel_scaling.y;\n }\n\n return null;\n });\n return result;\n }; // Given an offset in an img, in source image pixels, return the\n // corresponding offset in CSS pixels. If the img content is 1000 pixels\n // wide, but is scaled to 400 pixels on screen, and the input is x:1000,\n // then this will return x:400.\n\n\n coordmap.scaleImgToCss = function (offset_img) {\n var pixel_scaling = coordmap.imgToCssScalingRatio();\n var result = mapValues(offset_img, function (value, key) {\n var prefix = key.substring(0, 1);\n\n if (prefix === \"x\") {\n return offset_img[key] * pixel_scaling.x;\n } else if (prefix === \"y\") {\n return offset_img[key] * pixel_scaling.y;\n }\n\n return null;\n });\n return result;\n }; // Returns the x and y ratio the image content is scaled to on screen. If\n // the image data is 1000 pixels wide and is scaled to 300 pixels on screen,\n // then this returns 0.3. (Note the 300 pixels refers to CSS pixels.)\n\n\n coordmap.imgToCssScalingRatio = function () {\n var img_dims = findDims($img);\n return {\n x: img_dims.x / coordmap.dims.width,\n y: img_dims.y / coordmap.dims.height\n };\n };\n\n coordmap.cssToImgScalingRatio = function () {\n var res = coordmap.imgToCssScalingRatio();\n return {\n x: 1 / res.x,\n y: 1 / res.y\n };\n }; // Given an offset in css pixels, return an object representing which panel\n // it's in. The `expand` argument tells it to expand the panel area by that\n // many pixels. It's possible for an offset to be within more than one\n // panel, because of the `expand` value. If that's the case, find the\n // nearest panel.\n\n\n coordmap.getPanelCss = function (offset_css) {\n var expand = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;\n var offset_img = coordmap.scaleCssToImg(offset_css);\n var x = offset_img.x;\n var y = offset_img.y; // Convert expand from css pixels to img pixels\n\n var cssToImgRatio = coordmap.cssToImgScalingRatio();\n var expand_img = {\n x: expand * cssToImgRatio.x,\n y: expand * cssToImgRatio.y\n };\n var matches = []; // Panels that match\n\n var dists = []; // Distance of offset to each matching panel\n\n var b;\n var i;\n\n for (i = 0; i < coordmap.panels.length; i++) {\n b = coordmap.panels[i].range;\n\n if (x <= b.right + expand_img.x && x >= b.left - expand_img.x && y <= b.bottom + expand_img.y && y >= b.top - expand_img.y) {\n matches.push(coordmap.panels[i]); // Find distance from edges for x and y\n\n var xdist = 0;\n var ydist = 0;\n\n if (x > b.right && x <= b.right + expand_img.x) {\n xdist = x - b.right;\n } else if (x < b.left && x >= b.left - expand_img.x) {\n xdist = x - b.left;\n }\n\n if (y > b.bottom && y <= b.bottom + expand_img.y) {\n ydist = y - b.bottom;\n } else if (y < b.top && y >= b.top - expand_img.y) {\n ydist = y - b.top;\n } // Cartesian distance\n\n\n dists.push(Math.sqrt(Math.pow(xdist, 2) + Math.pow(ydist, 2)));\n }\n }\n\n if (matches.length) {\n // Find shortest distance\n var min_dist = Math.min.apply(null, dists);\n\n for (i = 0; i < matches.length; i++) {\n if (dists[i] === min_dist) {\n return matches[i];\n }\n }\n }\n\n return null;\n }; // Is an offset (in css pixels) in a panel? If supplied, `expand` tells us\n // to expand the panels by that many pixels in all directions.\n\n\n coordmap.isInPanelCss = function (offset_css) {\n var expand = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;\n if (coordmap.getPanelCss(offset_css, expand)) return true;\n return false;\n }; // Returns a function that sends mouse coordinates, scaled to data space.\n // If that function is passed a null event, it will send null.\n\n\n coordmap.mouseCoordinateSender = function (inputId, clip, nullOutside) {\n if (clip === undefined) clip = true;\n if (nullOutside === undefined) nullOutside = false;\n return function (e) {\n if (e === null) {\n Shiny.setInputValue(inputId, null);\n return;\n }\n\n var coords = {};\n var coords_css = coordmap.mouseOffsetCss(e); // If outside of plotting region\n\n if (!coordmap.isInPanelCss(coords_css)) {\n if (nullOutside) {\n Shiny.setInputValue(inputId, null);\n return;\n }\n\n if (clip) return;\n coords.coords_css = coords_css;\n coords.coords_img = coordmap.scaleCssToImg(coords_css);\n Shiny.setInputValue(inputId, coords, {\n priority: \"event\"\n });\n return;\n }\n\n var panel = coordmap.getPanelCss(coords_css);\n var coords_img = coordmap.scaleCssToImg(coords_css);\n var coords_data = panel.scaleImgToData(coords_img);\n coords.x = coords_data.x;\n coords.y = coords_data.y;\n coords.coords_css = coords_css;\n coords.coords_img = coords_img;\n coords.img_css_ratio = coordmap.cssToImgScalingRatio(); // Add the panel (facet) variables, if present\n\n $.extend(coords, panel.panel_vars); // Add variable name mappings\n\n coords.mapping = panel.mapping; // Add scaling information\n\n coords.domain = panel.domain;\n coords.range = panel.range;\n coords.log = panel.log;\n Shiny.setInputValue(inputId, coords, {\n priority: \"event\"\n });\n };\n };\n }; // Given two sets of x/y coordinates, return an object representing the min\n // and max x and y values. (This could be generalized to any number of\n // points).\n\n\n imageutils.findBox = function (offset1, offset2) {\n return {\n xmin: Math.min(offset1.x, offset2.x),\n xmax: Math.max(offset1.x, offset2.x),\n ymin: Math.min(offset1.y, offset2.y),\n ymax: Math.max(offset1.y, offset2.y)\n };\n }; // Shift an array of values so that they are within a min and max. The vals\n // will be shifted so that they maintain the same spacing internally. If the\n // range in vals is larger than the range of min and max, the result might not\n // make sense.\n\n\n imageutils.shiftToRange = function (vals, min, max) {\n if (!(vals instanceof Array)) vals = [vals];\n var maxval = Math.max.apply(null, vals);\n var minval = Math.min.apply(null, vals);\n var shiftAmount = 0;\n\n if (maxval > max) {\n shiftAmount = max - maxval;\n } else if (minval < min) {\n shiftAmount = min - minval;\n }\n\n var newvals = [];\n\n for (var i = 0; i < vals.length; i++) {\n newvals[i] = vals[i] + shiftAmount;\n }\n\n return newvals;\n }; // This object provides two public event listeners: mousedown, and\n // dblclickIE8.\n // We need to make sure that, when the image is listening for double-\n // clicks, that a double-click doesn't trigger two click events. We'll\n // trigger custom mousedown2 and dblclick2 events with this mousedown\n // listener.\n\n\n imageutils.createClickInfo = function ($el, dblclickId, dblclickDelay) {\n var clickTimer = null;\n var pending_e = null; // A pending mousedown2 event\n // Create a new event of type eventType (like 'mousedown2'), and trigger\n // it with the information stored in this.e.\n\n function triggerEvent(newEventType, e) {\n // Extract important info from e and construct a new event with type\n // eventType.\n var e2 = $.Event(newEventType, {\n which: e.which,\n pageX: e.pageX,\n pageY: e.pageY\n });\n $el.trigger(e2);\n }\n\n function triggerPendingMousedown2() {\n // It's possible that between the scheduling of a mousedown2 and the\n // time this callback is executed, someone else triggers a\n // mousedown2, so check for that.\n if (pending_e) {\n triggerEvent(\"mousedown2\", pending_e);\n pending_e = null;\n }\n } // Set a timer to trigger a mousedown2 event, using information from the\n // last recorded mousdown event.\n\n\n function scheduleMousedown2(e) {\n pending_e = e;\n clickTimer = setTimeout(function () {\n triggerPendingMousedown2();\n }, dblclickDelay);\n }\n\n function mousedown(e) {\n // Listen for left mouse button only\n if (e.which !== 1) return; // If no dblclick listener, immediately trigger a mousedown2 event.\n\n if (!dblclickId) {\n triggerEvent(\"mousedown2\", e);\n return;\n } // If there's a dblclick listener, make sure not to count this as a\n // click on the first mousedown; we need to wait for the dblclick\n // delay before we can be sure this click was a single-click.\n\n\n if (pending_e === null) {\n scheduleMousedown2(e);\n } else {\n clearTimeout(clickTimer); // If second click is too far away, it doesn't count as a double\n // click. Instead, immediately trigger a mousedown2 for the previous\n // click, and set this click as a new first click.\n\n if (pending_e && Math.abs(pending_e.pageX - e.pageX) > 2 || Math.abs(pending_e.pageY - e.pageY) > 2) {\n triggerPendingMousedown2();\n scheduleMousedown2(e);\n } else {\n // The second click was close to the first one. If it happened\n // within specified delay, trigger our custom 'dblclick2' event.\n pending_e = null;\n triggerEvent(\"dblclick2\", e);\n }\n }\n } // IE8 needs a special hack because when you do a double-click it doesn't\n // trigger the click event twice - it directly triggers dblclick.\n\n\n function dblclickIE8(e) {\n e.which = 1; // In IE8, e.which is 0 instead of 1. ???\n\n triggerEvent(\"dblclick2\", e);\n }\n\n return {\n mousedown: mousedown,\n dblclickIE8: dblclickIE8\n };\n }; // ----------------------------------------------------------\n // Handler creators for click, hover, brush.\n // Each of these returns an object with a few public members. These public\n // members are callbacks that are meant to be bound to events on $el with\n // the same name (like 'mousedown').\n // ----------------------------------------------------------\n\n\n imageutils.createClickHandler = function (inputId, clip, coordmap) {\n var clickInfoSender = coordmap.mouseCoordinateSender(inputId, clip);\n return {\n mousedown: function mousedown(e) {\n // Listen for left mouse button only\n if (e.which !== 1) return;\n clickInfoSender(e);\n },\n onResetImg: function onResetImg() {\n clickInfoSender(null);\n },\n onResize: null\n };\n };\n\n imageutils.createHoverHandler = function (inputId, delay, delayType, clip, nullOutside, coordmap) {\n var sendHoverInfo = coordmap.mouseCoordinateSender(inputId, clip, nullOutside);\n var hoverInfoSender;\n if (delayType === \"throttle\") hoverInfoSender = new Throttler(null, sendHoverInfo, delay);else hoverInfoSender = new Debouncer(null, sendHoverInfo, delay); // What to do when mouse exits the image\n\n var mouseout;\n if (nullOutside) mouseout = function mouseout() {\n hoverInfoSender.normalCall(null);\n };else mouseout = function mouseout() {};\n return {\n mousemove: function mousemove(e) {\n hoverInfoSender.normalCall(e);\n },\n mouseout: mouseout,\n onResetImg: function onResetImg() {\n hoverInfoSender.immediateCall(null);\n },\n onResize: null\n };\n }; // Returns a brush handler object. This has three public functions:\n // mousedown, mousemove, and onResetImg.\n\n\n imageutils.createBrushHandler = function (inputId, $el, opts, coordmap, outputId) {\n // Parameter: expand the area in which a brush can be started, by this\n // many pixels in all directions. (This should probably be a brush option)\n var expandPixels = 20; // Represents the state of the brush\n\n var brush = imageutils.createBrush($el, opts, coordmap, expandPixels); // Brush IDs can span multiple image/plot outputs. When an output is brushed,\n // if a brush with the same ID is active on a different image/plot, it must\n // be dismissed (but without sending any data to the server). We implement\n // this by sending the shiny-internal:brushed event to all plots, and letting\n // each plot decide for itself what to do.\n //\n // The decision to have the event sent to each plot (as opposed to a single\n // event triggered on, say, the document) was made to make cleanup easier;\n // listening on an event on the document would prevent garbage collection\n // of plot outputs that are removed from the document.\n\n $el.on(\"shiny-internal:brushed.image_output\", function (e, coords) {\n // If the new brush shares our ID but not our output element ID, we\n // need to clear our brush (if any).\n if (coords.brushId === inputId && coords.outputId !== outputId) {\n $el.data(\"mostRecentBrush\", false);\n brush.reset();\n }\n }); // Set cursor to one of 7 styles. We need to set the cursor on the whole\n // el instead of the brush div, because the brush div has\n // 'pointer-events:none' so that it won't intercept pointer events.\n // If `style` is null, don't add a cursor style.\n\n function setCursorStyle(style) {\n $el.removeClass(\"crosshair grabbable grabbing ns-resize ew-resize nesw-resize nwse-resize\");\n if (style) $el.addClass(style);\n }\n\n function sendBrushInfo() {\n var coords = brush.boundsData(); // We're in a new or reset state\n\n if (isNaN(coords.xmin)) {\n Shiny.setInputValue(inputId, null); // Must tell other brushes to clear.\n\n imageOutputBinding.find(document).trigger(\"shiny-internal:brushed\", {\n brushId: inputId,\n outputId: null\n });\n return;\n }\n\n var panel = brush.getPanel(); // Add the panel (facet) variables, if present\n\n $.extend(coords, panel.panel_vars);\n coords.coords_css = brush.boundsCss();\n coords.coords_img = coordmap.scaleCssToImg(coords.coords_css);\n coords.img_css_ratio = coordmap.cssToImgScalingRatio(); // Add variable name mappings\n\n coords.mapping = panel.mapping; // Add scaling information\n\n coords.domain = panel.domain;\n coords.range = panel.range;\n coords.log = panel.log;\n coords.direction = opts.brushDirection;\n coords.brushId = inputId;\n coords.outputId = outputId; // Send data to server\n\n Shiny.setInputValue(inputId, coords);\n $el.data(\"mostRecentBrush\", true);\n imageOutputBinding.find(document).trigger(\"shiny-internal:brushed\", coords);\n }\n\n var brushInfoSender;\n\n if (opts.brushDelayType === \"throttle\") {\n brushInfoSender = new Throttler(null, sendBrushInfo, opts.brushDelay);\n } else {\n brushInfoSender = new Debouncer(null, sendBrushInfo, opts.brushDelay);\n }\n\n function mousedown(e) {\n // This can happen when mousedown inside the graphic, then mouseup\n // outside, then mousedown inside. Just ignore the second\n // mousedown.\n if (brush.isBrushing() || brush.isDragging() || brush.isResizing()) return; // Listen for left mouse button only\n\n if (e.which !== 1) return; // In general, brush uses css pixels, and coordmap uses img pixels.\n\n var offset_css = coordmap.mouseOffsetCss(e); // Ignore mousedown events outside of plotting region, expanded by\n // a number of pixels specified in expandPixels.\n\n if (opts.brushClip && !coordmap.isInPanelCss(offset_css, expandPixels)) return;\n brush.up({\n x: NaN,\n y: NaN\n });\n brush.down(offset_css);\n\n if (brush.isInResizeArea(offset_css)) {\n brush.startResizing(offset_css); // Attach the move and up handlers to the window so that they respond\n // even when the mouse is moved outside of the image.\n\n $(document).on(\"mousemove.image_brush\", mousemoveResizing).on(\"mouseup.image_brush\", mouseupResizing);\n } else if (brush.isInsideBrush(offset_css)) {\n brush.startDragging(offset_css);\n setCursorStyle(\"grabbing\"); // Attach the move and up handlers to the window so that they respond\n // even when the mouse is moved outside of the image.\n\n $(document).on(\"mousemove.image_brush\", mousemoveDragging).on(\"mouseup.image_brush\", mouseupDragging);\n } else {\n var panel = coordmap.getPanelCss(offset_css, expandPixels);\n brush.startBrushing(panel.clipImg(coordmap.scaleCssToImg(offset_css))); // Attach the move and up handlers to the window so that they respond\n // even when the mouse is moved outside of the image.\n\n $(document).on(\"mousemove.image_brush\", mousemoveBrushing).on(\"mouseup.image_brush\", mouseupBrushing);\n }\n } // This sets the cursor style when it's in the el\n\n\n function mousemove(e) {\n // In general, brush uses css pixels, and coordmap uses img pixels.\n var offset_css = coordmap.mouseOffsetCss(e);\n\n if (!(brush.isBrushing() || brush.isDragging() || brush.isResizing())) {\n // Set the cursor depending on where it is\n if (brush.isInResizeArea(offset_css)) {\n var r = brush.whichResizeSides(offset_css);\n\n if (r.left && r.top || r.right && r.bottom) {\n setCursorStyle(\"nwse-resize\");\n } else if (r.left && r.bottom || r.right && r.top) {\n setCursorStyle(\"nesw-resize\");\n } else if (r.left || r.right) {\n setCursorStyle(\"ew-resize\");\n } else if (r.top || r.bottom) {\n setCursorStyle(\"ns-resize\");\n }\n } else if (brush.isInsideBrush(offset_css)) {\n setCursorStyle(\"grabbable\");\n } else if (coordmap.isInPanelCss(offset_css, expandPixels)) {\n setCursorStyle(\"crosshair\");\n } else {\n setCursorStyle(null);\n }\n }\n } // mousemove handlers while brushing or dragging\n\n\n function mousemoveBrushing(e) {\n brush.brushTo(coordmap.mouseOffsetCss(e));\n brushInfoSender.normalCall();\n }\n\n function mousemoveDragging(e) {\n brush.dragTo(coordmap.mouseOffsetCss(e));\n brushInfoSender.normalCall();\n }\n\n function mousemoveResizing(e) {\n brush.resizeTo(coordmap.mouseOffsetCss(e));\n brushInfoSender.normalCall();\n } // mouseup handlers while brushing or dragging\n\n\n function mouseupBrushing(e) {\n // Listen for left mouse button only\n if (e.which !== 1) return;\n $(document).off(\"mousemove.image_brush\").off(\"mouseup.image_brush\");\n brush.up(coordmap.mouseOffsetCss(e));\n brush.stopBrushing();\n setCursorStyle(\"crosshair\"); // If the brush didn't go anywhere, hide the brush, clear value,\n // and return.\n\n if (brush.down().x === brush.up().x && brush.down().y === brush.up().y) {\n brush.reset();\n brushInfoSender.immediateCall();\n return;\n } // Send info immediately on mouseup, but only if needed. If we don't\n // do the pending check, we might send the same data twice (with\n // with difference nonce).\n\n\n if (brushInfoSender.isPending()) brushInfoSender.immediateCall();\n }\n\n function mouseupDragging(e) {\n // Listen for left mouse button only\n if (e.which !== 1) return;\n $(document).off(\"mousemove.image_brush\").off(\"mouseup.image_brush\");\n brush.up(coordmap.mouseOffsetCss(e));\n brush.stopDragging();\n setCursorStyle(\"grabbable\");\n if (brushInfoSender.isPending()) brushInfoSender.immediateCall();\n }\n\n function mouseupResizing(e) {\n // Listen for left mouse button only\n if (e.which !== 1) return;\n $(document).off(\"mousemove.image_brush\").off(\"mouseup.image_brush\");\n brush.up(coordmap.mouseOffsetCss(e));\n brush.stopResizing();\n if (brushInfoSender.isPending()) brushInfoSender.immediateCall();\n } // Brush maintenance: When an image is re-rendered, the brush must either\n // be removed (if brushResetOnNew) or imported (if !brushResetOnNew). The\n // \"mostRecentBrush\" bit is to ensure that when multiple outputs share the\n // same brush ID, inactive brushes don't send null values up to the server.\n // This should be called when the img (not the el) is reset\n\n\n function onResetImg() {\n if (opts.brushResetOnNew) {\n if ($el.data(\"mostRecentBrush\")) {\n brush.reset();\n brushInfoSender.immediateCall();\n }\n }\n }\n\n if (!opts.brushResetOnNew) {\n if ($el.data(\"mostRecentBrush\")) {\n // Importing an old brush must happen after the image data has loaded\n // and the DOM element has the updated size. If importOldBrush()\n // is called before this happens, then the css-img coordinate mappings\n // will give the wrong result, and the brush will have the wrong\n // position.\n //\n // jcheng 09/26/2018: This used to happen in img.onLoad, but recently\n // we moved to all brush initialization moving to img.onLoad so this\n // logic can be executed inline.\n brush.importOldBrush();\n brushInfoSender.immediateCall();\n }\n }\n\n function onResize() {\n brush.onResize();\n brushInfoSender.immediateCall();\n }\n\n return {\n mousedown: mousedown,\n mousemove: mousemove,\n onResetImg: onResetImg,\n onResize: onResize\n };\n }; // Returns an object that represents the state of the brush. This gets wrapped\n // in a brushHandler, which provides various event listeners.\n\n\n imageutils.createBrush = function ($el, opts, coordmap, expandPixels) {\n // Number of pixels outside of brush to allow start resizing\n var resizeExpand = 10;\n var el = $el[0];\n var $div = null; // The div representing the brush\n\n var state = {}; // Aliases for conciseness\n\n var cssToImg = coordmap.scaleCssToImg;\n var imgToCss = coordmap.scaleImgToCss;\n reset();\n\n function reset() {\n // Current brushing/dragging/resizing state\n state.brushing = false;\n state.dragging = false;\n state.resizing = false; // Offset of last mouse down and up events (in CSS pixels)\n\n state.down = {\n x: NaN,\n y: NaN\n };\n state.up = {\n x: NaN,\n y: NaN\n }; // Which side(s) we're currently resizing\n\n state.resizeSides = {\n left: false,\n right: false,\n top: false,\n bottom: false\n }; // Bounding rectangle of the brush, in CSS pixel and data dimensions. We\n // need to record data dimensions along with pixel dimensions so that when\n // a new plot is sent, we can re-draw the brush div with the appropriate\n // coords.\n\n state.boundsCss = {\n xmin: NaN,\n xmax: NaN,\n ymin: NaN,\n ymax: NaN\n };\n state.boundsData = {\n xmin: NaN,\n xmax: NaN,\n ymin: NaN,\n ymax: NaN\n }; // Panel object that the brush is in\n\n state.panel = null; // The bounds at the start of a drag/resize (in CSS pixels)\n\n state.changeStartBounds = {\n xmin: NaN,\n xmax: NaN,\n ymin: NaN,\n ymax: NaN\n };\n if ($div) $div.remove();\n } // If there's an existing brush div, use that div to set the new brush's\n // settings, provided that the x, y, and panel variables have the same names,\n // and there's a panel with matching panel variable values.\n\n\n function importOldBrush() {\n var oldDiv = $el.find(\"#\" + el.id + \"_brush\");\n if (oldDiv.length === 0) return;\n var oldBoundsData = oldDiv.data(\"bounds-data\");\n var oldPanel = oldDiv.data(\"panel\");\n if (!oldBoundsData || !oldPanel) return; // Find a panel that has matching vars; if none found, we can't restore.\n // The oldPanel and new panel must match on their mapping vars, and the\n // values.\n\n for (var i = 0; i < coordmap.panels.length; i++) {\n var curPanel = coordmap.panels[i];\n\n if (equal(oldPanel.mapping, curPanel.mapping) && equal(oldPanel.panel_vars, curPanel.panel_vars)) {\n // We've found a matching panel\n state.panel = coordmap.panels[i];\n break;\n }\n } // If we didn't find a matching panel, remove the old div and return\n\n\n if (state.panel === null) {\n oldDiv.remove();\n return;\n }\n\n $div = oldDiv;\n boundsData(oldBoundsData);\n updateDiv();\n } // This will reposition the brush div when the image is resized, maintaining\n // the same data coordinates. Note that the \"resize\" here refers to the\n // wrapper div/img being resized; elsewhere, \"resize\" refers to the brush\n // div being resized.\n\n\n function onResize() {\n var bounds_data = boundsData(); // Check to see if we have valid boundsData\n\n for (var val in bounds_data) {\n if (isnan(bounds_data[val])) return;\n }\n\n boundsData(bounds_data);\n updateDiv();\n } // Return true if the offset is inside min/max coords\n\n\n function isInsideBrush(offset_css) {\n var bounds = state.boundsCss;\n return offset_css.x <= bounds.xmax && offset_css.x >= bounds.xmin && offset_css.y <= bounds.ymax && offset_css.y >= bounds.ymin;\n } // Return true if offset is inside a region to start a resize\n\n\n function isInResizeArea(offset_css) {\n var sides = whichResizeSides(offset_css);\n return sides.left || sides.right || sides.top || sides.bottom;\n } // Return an object representing which resize region(s) the cursor is in.\n\n\n function whichResizeSides(offset_css) {\n var b = state.boundsCss; // Bounds with expansion\n\n var e = {\n xmin: b.xmin - resizeExpand,\n xmax: b.xmax + resizeExpand,\n ymin: b.ymin - resizeExpand,\n ymax: b.ymax + resizeExpand\n };\n var res = {\n left: false,\n right: false,\n top: false,\n bottom: false\n };\n\n if ((opts.brushDirection === \"xy\" || opts.brushDirection === \"x\") && offset_css.y <= e.ymax && offset_css.y >= e.ymin) {\n if (offset_css.x < b.xmin && offset_css.x >= e.xmin) res.left = true;else if (offset_css.x > b.xmax && offset_css.x <= e.xmax) res.right = true;\n }\n\n if ((opts.brushDirection === \"xy\" || opts.brushDirection === \"y\") && offset_css.x <= e.xmax && offset_css.x >= e.xmin) {\n if (offset_css.y < b.ymin && offset_css.y >= e.ymin) res.top = true;else if (offset_css.y > b.ymax && offset_css.y <= e.ymax) res.bottom = true;\n }\n\n return res;\n } // Sets the bounds of the brush (in CSS pixels), given a box and optional\n // panel. This will fit the box bounds into the panel, so we don't brush\n // outside of it. This knows whether we're brushing in the x, y, or xy\n // directions, and sets bounds accordingly. If no box is passed in, just\n // return current bounds.\n\n\n function boundsCss(box_css) {\n if (box_css === undefined) {\n return $.extend({}, state.boundsCss);\n }\n\n var min_css = {\n x: box_css.xmin,\n y: box_css.ymin\n };\n var max_css = {\n x: box_css.xmax,\n y: box_css.ymax\n };\n var panel = state.panel;\n var panelBounds_img = panel.range;\n\n if (opts.brushClip) {\n min_css = imgToCss(panel.clipImg(cssToImg(min_css)));\n max_css = imgToCss(panel.clipImg(cssToImg(max_css)));\n }\n\n if (opts.brushDirection === \"xy\") {// No change\n } else if (opts.brushDirection === \"x\") {\n // Extend top and bottom of plotting area\n min_css.y = imgToCss({\n y: panelBounds_img.top\n }).y;\n max_css.y = imgToCss({\n y: panelBounds_img.bottom\n }).y;\n } else if (opts.brushDirection === \"y\") {\n min_css.x = imgToCss({\n x: panelBounds_img.left\n }).x;\n max_css.x = imgToCss({\n x: panelBounds_img.right\n }).x;\n }\n\n state.boundsCss = {\n xmin: min_css.x,\n xmax: max_css.x,\n ymin: min_css.y,\n ymax: max_css.y\n }; // Positions in data space\n\n var min_data = state.panel.scaleImgToData(cssToImg(min_css));\n var max_data = state.panel.scaleImgToData(cssToImg(max_css)); // For reversed scales, the min and max can be reversed, so use findBox\n // to ensure correct order.\n\n state.boundsData = imageutils.findBox(min_data, max_data); // Round to 14 significant digits to avoid spurious changes in FP values\n // (#1634).\n\n state.boundsData = mapValues(state.boundsData, function (val) {\n return roundSignif(val, 14);\n }); // We also need to attach the data bounds and panel as data attributes, so\n // that if the image is re-sent, we can grab the data bounds to create a new\n // brush. This should be fast because it doesn't actually modify the DOM.\n\n $div.data(\"bounds-data\", state.boundsData);\n $div.data(\"panel\", state.panel);\n return undefined;\n } // Get or set the bounds of the brush using coordinates in the data space.\n\n\n function boundsData(box_data) {\n if (box_data === undefined) {\n return $.extend({}, state.boundsData);\n }\n\n var box_css = imgToCss(state.panel.scaleDataToImg(box_data)); // Round to 13 significant digits to avoid spurious changes in FP values\n // (#2197).\n\n box_css = mapValues(box_css, function (val) {\n return roundSignif(val, 13);\n }); // The scaling function can reverse the direction of the axes, so we need to\n // find the min and max again.\n\n boundsCss({\n xmin: Math.min(box_css.xmin, box_css.xmax),\n xmax: Math.max(box_css.xmin, box_css.xmax),\n ymin: Math.min(box_css.ymin, box_css.ymax),\n ymax: Math.max(box_css.ymin, box_css.ymax)\n });\n return undefined;\n }\n\n function getPanel() {\n return state.panel;\n } // Add a new div representing the brush.\n\n\n function addDiv() {\n if ($div) $div.remove(); // Start hidden; we'll show it when movement occurs\n\n $div = $(document.createElement(\"div\")).attr(\"id\", el.id + \"_brush\").css({\n \"background-color\": opts.brushFill,\n opacity: opts.brushOpacity,\n \"pointer-events\": \"none\",\n position: \"absolute\"\n }).hide();\n var borderStyle = \"1px solid \" + opts.brushStroke;\n\n if (opts.brushDirection === \"xy\") {\n $div.css({\n border: borderStyle\n });\n } else if (opts.brushDirection === \"x\") {\n $div.css({\n \"border-left\": borderStyle,\n \"border-right\": borderStyle\n });\n } else if (opts.brushDirection === \"y\") {\n $div.css({\n \"border-top\": borderStyle,\n \"border-bottom\": borderStyle\n });\n }\n\n $el.append($div);\n $div.offset({\n x: 0,\n y: 0\n }).width(0).outerHeight(0);\n } // Update the brush div to reflect the current brush bounds.\n\n\n function updateDiv() {\n // Need parent offset relative to page to calculate mouse offset\n // relative to page.\n var img_offset_css = findOrigin($el.find(\"img\"));\n var b = state.boundsCss;\n $div.offset({\n top: img_offset_css.y + b.ymin,\n left: img_offset_css.x + b.xmin\n }).outerWidth(b.xmax - b.xmin + 1).outerHeight(b.ymax - b.ymin + 1);\n }\n\n function down(offset_css) {\n if (offset_css === undefined) return state.down;\n state.down = offset_css;\n return undefined;\n }\n\n function up(offset_css) {\n if (offset_css === undefined) return state.up;\n state.up = offset_css;\n return undefined;\n }\n\n function isBrushing() {\n return state.brushing;\n }\n\n function startBrushing() {\n state.brushing = true;\n addDiv();\n state.panel = coordmap.getPanelCss(state.down, expandPixels);\n boundsCss(imageutils.findBox(state.down, state.down));\n updateDiv();\n }\n\n function brushTo(offset_css) {\n boundsCss(imageutils.findBox(state.down, offset_css));\n $div.show();\n updateDiv();\n }\n\n function stopBrushing() {\n state.brushing = false; // Save the final bounding box of the brush\n\n boundsCss(imageutils.findBox(state.down, state.up));\n }\n\n function isDragging() {\n return state.dragging;\n }\n\n function startDragging() {\n state.dragging = true;\n state.changeStartBounds = $.extend({}, state.boundsCss);\n }\n\n function dragTo(offset_css) {\n // How far the brush was dragged\n var dx = offset_css.x - state.down.x;\n var dy = offset_css.y - state.down.y; // Calculate what new positions would be, before clipping.\n\n var start = state.changeStartBounds;\n var newBounds_css = {\n xmin: start.xmin + dx,\n xmax: start.xmax + dx,\n ymin: start.ymin + dy,\n ymax: start.ymax + dy\n }; // Clip to the plotting area\n\n if (opts.brushClip) {\n var panelBounds_img = state.panel.range;\n var newBounds_img = cssToImg(newBounds_css); // Convert to format for shiftToRange\n\n var xvals_img = [newBounds_img.xmin, newBounds_img.xmax];\n var yvals_img = [newBounds_img.ymin, newBounds_img.ymax];\n xvals_img = imageutils.shiftToRange(xvals_img, panelBounds_img.left, panelBounds_img.right);\n yvals_img = imageutils.shiftToRange(yvals_img, panelBounds_img.top, panelBounds_img.bottom); // Convert back to bounds format\n\n newBounds_css = imgToCss({\n xmin: xvals_img[0],\n xmax: xvals_img[1],\n ymin: yvals_img[0],\n ymax: yvals_img[1]\n });\n }\n\n boundsCss(newBounds_css);\n updateDiv();\n }\n\n function stopDragging() {\n state.dragging = false;\n }\n\n function isResizing() {\n return state.resizing;\n }\n\n function startResizing() {\n state.resizing = true;\n state.changeStartBounds = $.extend({}, state.boundsCss);\n state.resizeSides = whichResizeSides(state.down);\n }\n\n function resizeTo(offset_css) {\n // How far the brush was dragged\n var d_css = {\n x: offset_css.x - state.down.x,\n y: offset_css.y - state.down.y\n };\n var d_img = cssToImg(d_css); // Calculate what new positions would be, before clipping.\n\n var b_img = cssToImg(state.changeStartBounds);\n var panelBounds_img = state.panel.range;\n\n if (state.resizeSides.left) {\n var xmin_img = imageutils.shiftToRange(b_img.xmin + d_img.x, panelBounds_img.left, b_img.xmax)[0];\n b_img.xmin = xmin_img;\n } else if (state.resizeSides.right) {\n var xmax_img = imageutils.shiftToRange(b_img.xmax + d_img.x, b_img.xmin, panelBounds_img.right)[0];\n b_img.xmax = xmax_img;\n }\n\n if (state.resizeSides.top) {\n var ymin_img = imageutils.shiftToRange(b_img.ymin + d_img.y, panelBounds_img.top, b_img.ymax)[0];\n b_img.ymin = ymin_img;\n } else if (state.resizeSides.bottom) {\n var ymax_img = imageutils.shiftToRange(b_img.ymax + d_img.y, b_img.ymin, panelBounds_img.bottom)[0];\n b_img.ymax = ymax_img;\n }\n\n boundsCss(imgToCss(b_img));\n updateDiv();\n }\n\n function stopResizing() {\n state.resizing = false;\n }\n\n return {\n reset: reset,\n importOldBrush: importOldBrush,\n isInsideBrush: isInsideBrush,\n isInResizeArea: isInResizeArea,\n whichResizeSides: whichResizeSides,\n onResize: onResize,\n // A callback when the wrapper div or img is resized.\n boundsCss: boundsCss,\n boundsData: boundsData,\n getPanel: getPanel,\n down: down,\n up: up,\n isBrushing: isBrushing,\n startBrushing: startBrushing,\n brushTo: brushTo,\n stopBrushing: stopBrushing,\n isDragging: isDragging,\n startDragging: startDragging,\n dragTo: dragTo,\n stopDragging: stopDragging,\n isResizing: isResizing,\n startResizing: startResizing,\n resizeTo: resizeTo,\n stopResizing: stopResizing\n };\n };\n\n Shiny.resetBrush = function (brushId) {\n Shiny.setInputValue(brushId, null);\n imageOutputBinding.find(document).trigger(\"shiny-internal:brushed\", {\n brushId: brushId,\n outputId: null\n });\n }; // -----------------------------------------------------------------------\n // Utility functions for finding dimensions and locations of DOM elements\n // -----------------------------------------------------------------------\n // Returns the ratio that an element has been scaled (for example, by CSS\n // transforms) in the x and y directions.\n\n\n function findScalingRatio($el) {\n var boundingRect = $el[0].getBoundingClientRect();\n return {\n x: boundingRect.width / $el.outerWidth(),\n y: boundingRect.height / $el.outerHeight()\n };\n }\n\n function findOrigin($el) {\n var offset = $el.offset();\n var scaling_ratio = findScalingRatio($el); // Find the size of the padding and border, for the top and left. This is\n // before any transforms.\n\n var paddingBorder = {\n left: parseInt($el.css(\"border-left-width\")) + parseInt($el.css(\"padding-left\")),\n top: parseInt($el.css(\"border-top-width\")) + parseInt($el.css(\"padding-top\"))\n }; // offset() returns the upper left corner of the element relative to the\n // page, but it includes padding and border. Here we find the upper left\n // of the element, not including padding and border.\n\n return {\n x: offset.left + scaling_ratio.x * paddingBorder.left,\n y: offset.top + scaling_ratio.y * paddingBorder.top\n };\n } // Find the dimensions of a tag, after transforms, and without padding and\n // border.\n\n\n function findDims($el) {\n // If there's any padding/border, we need to find the ratio of the actual\n // element content compared to the element plus padding and border.\n var content_ratio = {\n x: $el.width() / $el.outerWidth(),\n y: $el.height() / $el.outerHeight()\n }; // Get the dimensions of the element _after_ any CSS transforms. This\n // includes the padding and border.\n\n var bounding_rect = $el[0].getBoundingClientRect(); // Dimensions of the element after any CSS transforms, and without\n // padding/border.\n\n return {\n x: content_ratio.x * bounding_rect.width,\n y: content_ratio.y * bounding_rect.height\n };\n } // \"output_binding_html.js\"\n\n\n var htmlOutputBinding = new OutputBinding();\n $.extend(htmlOutputBinding, {\n find: function find(scope) {\n return $(scope).find(\".shiny-html-output\");\n },\n onValueError: function onValueError(el, err) {\n Shiny.unbindAll(el);\n this.renderError(el, err);\n },\n renderValue: function renderValue(el, data) {\n Shiny.renderContent(el, data);\n }\n });\n outputBindings.register(htmlOutputBinding, \"shiny.htmlOutput\");\n\n var renderDependencies = Shiny.renderDependencies = function (dependencies) {\n if (dependencies) {\n $.each(dependencies, function (i, dep) {\n renderDependency(dep);\n });\n }\n }; // Render HTML in a DOM element, add dependencies, and bind Shiny\n // inputs/outputs. `content` can be null, a string, or an object with\n // properties 'html' and 'deps'.\n\n\n Shiny.renderContent = function (el, content) {\n var where = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : \"replace\";\n\n if (where === \"replace\") {\n Shiny.unbindAll(el);\n }\n\n var html;\n var dependencies = [];\n\n if (content === null) {\n html = \"\";\n } else if (typeof content === \"string\") {\n html = content;\n } else if (_typeof(content) === \"object\") {\n html = content.html;\n dependencies = content.deps || [];\n }\n\n Shiny.renderHtml(html, el, dependencies, where);\n var scope = el;\n\n if (where === \"replace\") {\n Shiny.initializeInputs(el);\n Shiny.bindAll(el);\n } else {\n var $parent = $(el).parent();\n\n if ($parent.length > 0) {\n scope = $parent;\n\n if (where === \"beforeBegin\" || where === \"afterEnd\") {\n var $grandparent = $parent.parent();\n if ($grandparent.length > 0) scope = $grandparent;\n }\n }\n\n Shiny.initializeInputs(scope);\n Shiny.bindAll(scope);\n }\n }; // Render HTML in a DOM element, inserting singletons into head as needed\n\n\n Shiny.renderHtml = function (html, el, dependencies) {\n var where = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : \"replace\";\n renderDependencies(dependencies);\n return singletons.renderHtml(html, el, where);\n };\n\n var htmlDependencies = {};\n\n function registerDependency(name, version) {\n htmlDependencies[name] = version;\n } // Re-render stylesheet(s) if the dependency has specificially requested it\n // and it matches an existing dependency (name and version)\n\n\n function needsRestyle(dep) {\n if (!dep.restyle) {\n return false;\n }\n\n var names = Object.keys(htmlDependencies);\n var idx = names.indexOf(dep.name);\n\n if (idx === -1) {\n return false;\n }\n\n return htmlDependencies[names[idx]] === dep.version;\n } // Client-side dependency resolution and rendering\n\n\n function renderDependency(dep) {\n var restyle = needsRestyle(dep);\n if (htmlDependencies.hasOwnProperty(dep.name) && !restyle) return false;\n registerDependency(dep.name, dep.version);\n var href = dep.src.href;\n var $head = $(\"head\").first();\n\n if (dep.meta && !restyle) {\n var metas = $.map(asArray(dep.meta), function (obj, idx) {\n // only one named pair is expected in obj as it's already been decomposed\n var name = Object.keys(obj)[0];\n return $(\"\").attr(\"name\", name).attr(\"content\", obj[name]);\n });\n $head.append(metas);\n }\n\n if (dep.stylesheet) {\n var links = $.map(asArray(dep.stylesheet), function (stylesheet) {\n return $(\"\").attr(\"href\", href + \"/\" + encodeURI(stylesheet));\n });\n\n if (!restyle) {\n $head.append(links);\n } else {\n // This inline