diff --git a/DESCRIPTION b/DESCRIPTION index 0b699d35..54e473ec 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: htmlwidgets Type: Package Title: HTML Widgets for R -Version: 1.4.0.9000 +Version: 1.5.1.9002 Authors@R: c( person("Ramnath", "Vaidyanathan", role = c("aut", "cph")), person("Yihui", "Xie", role = c("aut")), @@ -25,4 +25,4 @@ Suggests: Enhances: shiny (>= 1.1) URL: https://github.com/ramnathv/htmlwidgets BugReports: https://github.com/ramnathv/htmlwidgets/issues -RoxygenNote: 6.1.1 +RoxygenNote: 7.1.1 diff --git a/NAMESPACE b/NAMESPACE index c37ce80f..160e645b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,6 +4,7 @@ S3method(as.tags,htmlwidget) S3method(print,htmlwidget) S3method(print,suppress_viewer) export(JS) +export(JSEvals) export(appendContent) export(createWidget) export(getDependency) diff --git a/R/htmlwidgets.R b/R/htmlwidgets.R index aff81807..1eab89b5 100644 --- a/R/htmlwidgets.R +++ b/R/htmlwidgets.R @@ -358,6 +358,8 @@ createWidget <- function(name, #' function. #' @param reportSize Should the widget's container size be reported in the #' shiny session's client data? +#' @param reportTheme Should the widget's container styles (e.g., colors and fonts) +#' be reported in the shiny session's client data? #' @param expr An expression that generates an HTML widget (or a #' \href{https://rstudio.github.io/promises/}{promise} of an HTML widget). #' @param env The environment in which to evaluate \code{expr}. @@ -386,13 +388,27 @@ createWidget <- function(name, #' #' @export shinyWidgetOutput <- function(outputId, name, width, height, package = name, - inline = FALSE, reportSize = FALSE) { + inline = FALSE, reportSize = FALSE, reportTheme = FALSE) { checkShinyVersion() + + # Theme reporting requires this shiny feature + # https://github.com/rstudio/shiny/pull/2740/files + if (reportTheme && + nzchar(system.file(package = "shiny")) && + packageVersion("shiny") < "1.4.0.9003") { + message("`reportTheme = TRUE` requires shiny v.1.4.0.9003 or higher. Consider upgrading shiny.") + } + # generate html html <- htmltools::tagList( - widget_html(name, package, id = outputId, - class = paste0(name, " html-widget html-widget-output", if (reportSize) " shiny-report-size"), + widget_html( + name, package, id = outputId, + class = paste0( + name, " html-widget html-widget-output", + if (reportSize) " shiny-report-size", + if (reportTheme) " shiny-report-theme" + ), style = sprintf("width:%s; height:%s; %s", htmltools::validateCssUnit(width), htmltools::validateCssUnit(height), diff --git a/R/utils.R b/R/utils.R index 3a7de263..6d5a107c 100644 --- a/R/utils.R +++ b/R/utils.R @@ -128,19 +128,29 @@ JS <- function(...) { structure(x, class = unique(c("JS_EVAL", oldClass(x)))) } -# Creates a list of keys whose values need to be evaluated on the client-side. -# -# It works by transforming \code{list(foo = list(1, list(bar = -# I('function(){}')), 2))} to \code{list("foo.2.bar")}. Later on the JS side, we -# will split foo.2.bar to ['foo', '2', 'bar'] and evaluate the JSON object -# member. Note '2' (character) should have been 2 (integer) but it does not seem -# to matter in JS: x[2] is the same as x['2'] when all child members of x are -# unnamed, and ('2' in x) will be true even if x is an array without names. This -# is a little hackish. -# -# @param list a list in which the elements that should be evaluated as -# JavaScript are to be identified -# @author Yihui Xie +#' Creates a list of keys whose values need to be evaluated on the client-side +#' +#' It works by transforming \code{list(foo = list(1, list(bar = +#' I('function(){}')), 2))} to \code{list("foo.2.bar")}. Later on the JS side, +#' the \code{window.HTMLWidgets.evaluateStringMember} function is called with +#' the JSON object and the "foo.2.bar" string, which is split to \code{['foo', +#' '2', 'bar']}, and the string at that location is replaced \emph{in-situ} with +#' the results of evaluating it. Note '2' (character) should have been 2 +#' (integer) but it does not seem to matter in JS: x[2] is the same as x['2'] +#' when all child members of x are unnamed, and ('2' in x) will be true even if +#' x is an array without names. This is a little hackish. +#' +#' This function is intended mostly for internal use. There's generally no need +#' for widget authors or users to call it, as it's called automatically on the +#' widget instance data during rendering. It's exported in case other packages +#' want to add support for \code{\link{JS}} in contexts outside of widget +#' payloads. +#' +#' @param list a list in which the elements that should be evaluated as +#' JavaScript are to be identified +#' @author Yihui Xie +#' @keywords internal +#' @export JSEvals <- function(list) { # the `%||% list()` part is necessary as of R 3.4.0 (April 2017) -- if `evals` # is NULL then `I(evals)` results in a warning in R 3.4.0. This is circumvented diff --git a/inst/NEWS b/inst/NEWS index 07374296..8c90f28d 100644 --- a/inst/NEWS +++ b/inst/NEWS @@ -1,3 +1,17 @@ +htmlwidgets 1.5.1.9000 +------------------------------------------------------- + +* Fixed an issue with passing named function declarations to `JS()` and `onRender()` (introduced by v1.4). (#356) + +* Added a `reportTheme` argument to `shinyWidgetOutput()`. If `TRUE`, CSS styles of the widget's output container are made available to `shiny::getCurrentOutputInfo()`, making it possible to provide 'smart' styling defaults in a `renderWidget()` context. (#361) + +* Export the `JSEvals` function, allowing other packages to support `JS()` in non-widget contexts. + +htmlwidgets 1.5.1 +------------------------------------------------------- + +* Fixed an issue with dynamically rendered widgets (i.e., using `shiny::uiOutput()` to render a widget) with any version of shiny prior to 1.4. This issue was introduced by htmlwidgets 1.5. (#351) + htmlwidgets 1.5 ----------------------------------------------------------------------- diff --git a/inst/www/htmlwidgets.js b/inst/www/htmlwidgets.js index 64c7e8d7..3d227624 100644 --- a/inst/www/htmlwidgets.js +++ b/inst/www/htmlwidgets.js @@ -249,13 +249,13 @@ function tryEval(code) { var result = null; try { - result = eval(code); + result = eval("(" + code + ")"); } catch(error) { if (!error instanceof SyntaxError) { throw error; } try { - result = eval("(" + code + ")"); + result = eval(code); } catch(e) { if (e instanceof SyntaxError) { throw error; @@ -659,44 +659,56 @@ invokePostRenderHandlers(); } - // Wait until after the document has loaded to render the widgets. - if (shinyMode && window.jQuery) { - /* - / Shiny 1.4.0 bumps jQuery from 1.x to 3.x, which means jQuery's - / on-ready handler (i.e., $(fn)) is now asyncronous (i.e., it now - / really means $(setTimeout(fn)). https://jquery.com/upgrade-guide/3.0/#breaking-change-document-ready-handlers-are-now-asynchronous - / - / In order to ensure the order of execution of on-ready handlers - / remains consistent with how it's been in the past, a static render - / in Shiny is now scheduled via $(). - / - / This is not a long term solution: it just preserves the current order - / of execution. Part of that ordering is to ensure initShiny executes - / _after_ staticRender, which is both right and wrong: - / * It's wrong because, when initShiny executes, it registers methods - / like Shiny.onInputChange that widget authors would expect to be available - / during a staticRender. - / * It's also 'right' because initShiny currently (as of v1.4.0) wants - / to execute after user code so that it can bind to any dynamically - / created elements. - / - / A longer term solution might be to make changes to Shiny so that - / these methods are available before the binding takes place, and then - / the ordering would be: register methods -> static render -> bind. - */ - window.jQuery(function() { + + function has_jQuery3() { + if (!window.jQuery) { + return false; + } + var $version = window.jQuery.fn.jquery; + var $major_version = parseInt($version.split(".")[0]); + return $major_version >= 3; + } + + /* + / Shiny 1.4 bumped jQuery from 1.x to 3.x which means jQuery's + / on-ready handler (i.e., $(fn)) is now asyncronous (i.e., it now + / really means $(setTimeout(fn)). + / https://jquery.com/upgrade-guide/3.0/#breaking-change-document-ready-handlers-are-now-asynchronous + / + / Since Shiny uses $() to schedule initShiny, shiny>=1.4 calls initShiny + / one tick later than it did before, which means staticRender() is + / called renderValue() earlier than (advanced) widget authors might be expecting. + / https://github.com/rstudio/shiny/issues/2630 + / + / For a concrete example, leaflet has some methods (e.g., updateBounds) + / which reference Shiny methods registered in initShiny (e.g., setInputValue). + / Since leaflet is privy to this life-cycle, it knows to use setTimeout() to + / delay execution of those methods (until Shiny methods are ready) + / https://github.com/rstudio/leaflet/blob/18ec981/javascript/src/index.js#L266-L268 + / + / Ideally widget authors wouldn't need to use this setTimeout() hack that + / leaflet uses to call Shiny methods on a staticRender(). In the long run, + / the logic initShiny should be broken up so that method registration happens + / right away, but binding happens later. + */ + function maybeStaticRenderLater() { + if (shinyMode && has_jQuery3()) { + window.jQuery(window.HTMLWidgets.staticRender); + } else { window.HTMLWidgets.staticRender(); - }); - } else if (document.addEventListener) { + } + } + + if (document.addEventListener) { document.addEventListener("DOMContentLoaded", function() { document.removeEventListener("DOMContentLoaded", arguments.callee, false); - window.HTMLWidgets.staticRender(); + maybeStaticRenderLater(); }, false); } else if (document.attachEvent) { document.attachEvent("onreadystatechange", function() { if (document.readyState === "complete") { document.detachEvent("onreadystatechange", arguments.callee); - window.HTMLWidgets.staticRender(); + maybeStaticRenderLater(); } }); } diff --git a/man/JSEvals.Rd b/man/JSEvals.Rd new file mode 100644 index 00000000..210d76e7 --- /dev/null +++ b/man/JSEvals.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{JSEvals} +\alias{JSEvals} +\title{Creates a list of keys whose values need to be evaluated on the client-side} +\usage{ +JSEvals(list) +} +\arguments{ +\item{list}{a list in which the elements that should be evaluated as +JavaScript are to be identified} +} +\description{ +It works by transforming \code{list(foo = list(1, list(bar = +I('function(){}')), 2))} to \code{list("foo.2.bar")}. Later on the JS side, +the \code{window.HTMLWidgets.evaluateStringMember} function is called with +the JSON object and the "foo.2.bar" string, which is split to \code{['foo', +'2', 'bar']}, and the string at that location is replaced \emph{in-situ} with +the results of evaluating it. Note '2' (character) should have been 2 +(integer) but it does not seem to matter in JS: x[2] is the same as x['2'] +when all child members of x are unnamed, and ('2' in x) will be true even if +x is an array without names. This is a little hackish. +} +\details{ +This function is intended mostly for internal use. There's generally no need +for widget authors or users to call it, as it's called automatically on the +widget instance data during rendering. It's exported in case other packages +want to add support for \code{\link{JS}} in contexts outside of widget +payloads. +} +\author{ +Yihui Xie +} +\keyword{internal} diff --git a/man/createWidget.Rd b/man/createWidget.Rd index ee77f9d2..413f318c 100644 --- a/man/createWidget.Rd +++ b/man/createWidget.Rd @@ -4,9 +4,17 @@ \alias{createWidget} \title{Create an HTML Widget} \usage{ -createWidget(name, x, width = NULL, height = NULL, - sizingPolicy = htmlwidgets::sizingPolicy(), package = name, - dependencies = NULL, elementId = NULL, preRenderHook = NULL) +createWidget( + name, + x, + width = NULL, + height = NULL, + sizingPolicy = htmlwidgets::sizingPolicy(), + package = name, + dependencies = NULL, + elementId = NULL, + preRenderHook = NULL +) } \arguments{ \item{name}{Widget name (should match the base name of the YAML and diff --git a/man/htmlwidgets-shiny.Rd b/man/htmlwidgets-shiny.Rd index a779ab73..e641cfb3 100644 --- a/man/htmlwidgets-shiny.Rd +++ b/man/htmlwidgets-shiny.Rd @@ -6,8 +6,16 @@ \alias{shinyRenderWidget} \title{Shiny bindings for HTML widgets} \usage{ -shinyWidgetOutput(outputId, name, width, height, package = name, - inline = FALSE, reportSize = FALSE) +shinyWidgetOutput( + outputId, + name, + width, + height, + package = name, + inline = FALSE, + reportSize = FALSE, + reportTheme = FALSE +) shinyRenderWidget(expr, outputFunction, env, quoted) } @@ -28,6 +36,9 @@ for the output} \item{reportSize}{Should the widget's container size be reported in the shiny session's client data?} +\item{reportTheme}{Should the widget's container styles (e.g., colors and fonts) +be reported in the shiny session's client data?} + \item{expr}{An expression that generates an HTML widget (or a \href{https://rstudio.github.io/promises/}{promise} of an HTML widget).} diff --git a/man/saveWidget.Rd b/man/saveWidget.Rd index d5555625..62d6032d 100644 --- a/man/saveWidget.Rd +++ b/man/saveWidget.Rd @@ -4,9 +4,15 @@ \alias{saveWidget} \title{Save a widget to an HTML file} \usage{ -saveWidget(widget, file, selfcontained = TRUE, libdir = NULL, - background = "white", title = class(widget)[[1]], - knitrOptions = list()) +saveWidget( + widget, + file, + selfcontained = TRUE, + libdir = NULL, + background = "white", + title = class(widget)[[1]], + knitrOptions = list() +) } \arguments{ \item{widget}{Widget to save} diff --git a/man/sizingPolicy.Rd b/man/sizingPolicy.Rd index 39ff985e..fcaef019 100644 --- a/man/sizingPolicy.Rd +++ b/man/sizingPolicy.Rd @@ -4,15 +4,25 @@ \alias{sizingPolicy} \title{Create a widget sizing policy} \usage{ -sizingPolicy(defaultWidth = NULL, defaultHeight = NULL, - padding = NULL, viewer.defaultWidth = NULL, - viewer.defaultHeight = NULL, viewer.padding = NULL, - viewer.fill = TRUE, viewer.suppress = FALSE, - viewer.paneHeight = NULL, browser.defaultWidth = NULL, - browser.defaultHeight = NULL, browser.padding = NULL, - browser.fill = FALSE, browser.external = FALSE, - knitr.defaultWidth = NULL, knitr.defaultHeight = NULL, - knitr.figure = TRUE) +sizingPolicy( + defaultWidth = NULL, + defaultHeight = NULL, + padding = NULL, + viewer.defaultWidth = NULL, + viewer.defaultHeight = NULL, + viewer.padding = NULL, + viewer.fill = TRUE, + viewer.suppress = FALSE, + viewer.paneHeight = NULL, + browser.defaultWidth = NULL, + browser.defaultHeight = NULL, + browser.padding = NULL, + browser.fill = FALSE, + browser.external = FALSE, + knitr.defaultWidth = NULL, + knitr.defaultHeight = NULL, + knitr.figure = TRUE +) } \arguments{ \item{defaultWidth}{The default width used to display the widget. This diff --git a/vignettes/develop_intro.Rmd b/vignettes/develop_intro.Rmd index bd37a7e6..fdaa5a25 100644 --- a/vignettes/develop_intro.Rmd +++ b/vignettes/develop_intro.Rmd @@ -278,8 +278,8 @@ This method is highly recommended as it ensures that you get started with the ri ```r devtools::create("mywidget") # create package using devtools -setwd("mywidget") # navigate to package dir htmlwidgets::scaffoldWidget("mywidget") # create widget scaffolding +devtools::document() # roxygenize, so NAMESPACE is updated devtools::install() # install the package so we can try it ```