diff --git a/NEWS.md b/NEWS.md
index 8a8dbacbb..3bd596120 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -6,6 +6,8 @@
* `textAreaInput()` gains a `autoresize` option, which automatically resizes the text area to fit its content. (#4210)
+* The family of `update*Input()` functions can now render HTML content passed to the `label` argument (e.g., `updateInputText(label = tags$b("New label"))`). (#3996)
+
## Changes
* Shiny no longer suspends input changes when _any_ ` ` or `` is on the page. Instead, it now only suspends when a `submitButton()` is present. If you have reason for creating a submit button from custom HTML, add a CSS class of `shiny-submit-button` to the button. (#4209)
diff --git a/R/update-input.R b/R/update-input.R
index 24c591168..d36b0e908 100644
--- a/R/update-input.R
+++ b/R/update-input.R
@@ -37,7 +37,11 @@
updateTextInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL, placeholder = NULL) {
validate_session_object(session)
- message <- dropNulls(list(label=label, value=value, placeholder=placeholder))
+ message <- dropNulls(list(
+ label = processDeps(label, session),
+ value = value,
+ placeholder = placeholder
+ ))
session$sendInputMessage(inputId, message)
}
@@ -111,7 +115,10 @@ updateTextAreaInput <- updateTextInput
updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL) {
validate_session_object(session)
- message <- dropNulls(list(label=label, value=value))
+ message <- dropNulls(list(
+ label = processDeps(label, session),
+ value = value
+ ))
session$sendInputMessage(inputId, message)
}
@@ -175,13 +182,17 @@ updateActionButton <- function(session = getDefaultReactiveDomain(), inputId, la
validate_session_object(session)
if (!is.null(icon)) icon <- as.character(validateIcon(icon))
- message <- dropNulls(list(label=label, icon=icon, disabled=disabled))
+ message <- dropNulls(list(
+ label = processDeps(label, session),
+ icon = icon,
+ disabled = disabled
+ ))
session$sendInputMessage(inputId, message)
}
#' @rdname updateActionButton
#' @export
updateActionLink <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL) {
- updateActionButton(session, inputId=inputId, label=label, icon=icon)
+ updateActionButton(session, inputId = inputId, label = processDeps(label, session), icon = icon)
}
@@ -225,7 +236,12 @@ updateDateInput <- function(session = getDefaultReactiveDomain(), inputId, label
min <- dateYMD(min, "min")
max <- dateYMD(max, "max")
- message <- dropNulls(list(label=label, value=value, min=min, max=max))
+ message <- dropNulls(list(
+ label = processDeps(label, session),
+ value = value,
+ min = min,
+ max = max
+ ))
session$sendInputMessage(inputId, message)
}
@@ -275,7 +291,7 @@ updateDateRangeInput <- function(session = getDefaultReactiveDomain(), inputId,
max <- dateYMD(max, "max")
message <- dropNulls(list(
- label = label,
+ label = processDeps(label, session),
value = dropNulls(list(start = start, end = end)),
min = min,
max = max
@@ -374,13 +390,16 @@ updateNavlistPanel <- updateTabsetPanel
#' }
#' @export
updateNumericInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL,
- min = NULL, max = NULL, step = NULL) {
+ min = NULL, max = NULL, step = NULL) {
validate_session_object(session)
message <- dropNulls(list(
- label = label, value = formatNoSci(value),
- min = formatNoSci(min), max = formatNoSci(max), step = formatNoSci(step)
+ label = processDeps(label, session),
+ value = formatNoSci(value),
+ min = formatNoSci(min),
+ max = formatNoSci(max),
+ step = formatNoSci(step)
))
session$sendInputMessage(inputId, message)
}
@@ -460,7 +479,7 @@ updateSliderInput <- function(session = getDefaultReactiveDomain(), inputId, lab
}
message <- dropNulls(list(
- label = label,
+ label = processDeps(label, session),
value = formatNoSci(value),
min = formatNoSci(min),
max = formatNoSci(max),
@@ -491,7 +510,11 @@ updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
))
}
- message <- dropNulls(list(label = label, options = options, value = selected))
+ message <- dropNulls(list(
+ label = processDeps(label, session),
+ options = options,
+ value = selected
+ ))
session$sendInputMessage(inputId, message)
}
@@ -644,7 +667,11 @@ updateSelectInput <- function(session = getDefaultReactiveDomain(), inputId, lab
choices <- if (!is.null(choices)) choicesWithNames(choices)
if (!is.null(selected)) selected <- as.character(selected)
options <- if (!is.null(choices)) selectOptions(choices, selected, inputId, FALSE)
- message <- dropNulls(list(label = label, options = options, value = selected))
+ message <- dropNulls(list(
+ label = processDeps(label, session),
+ options = options,
+ value = selected
+ ))
session$sendInputMessage(inputId, message)
}
diff --git a/inst/www/shared/shiny.js b/inst/www/shared/shiny.js
index 8d0417ebb..db539ba3f 100644
--- a/inst/www/shared/shiny.js
+++ b/inst/www/shared/shiny.js
@@ -143,7 +143,631 @@
var import_jquery39 = __toESM(require_jquery());
// srcts/src/utils/index.ts
+ var import_jquery6 = __toESM(require_jquery());
+
+ // srcts/src/shiny/render.ts
+ var import_jquery5 = __toESM(require_jquery());
+
+ // srcts/src/shiny/initedMethods.ts
+ var fullShinyObj;
+ function setShinyObj(shiny) {
+ fullShinyObj = shiny;
+ }
+ function validateShinyHasBeenSet() {
+ if (typeof fullShinyObj === "undefined") {
+ throw "Shiny has not finish initialization yet. Please wait for the 'shiny-initialized' event.";
+ }
+ return fullShinyObj;
+ }
+ function shinySetInputValue(name, value, opts) {
+ validateShinyHasBeenSet().setInputValue(name, value, opts);
+ }
+ function shinyShinyApp() {
+ return validateShinyHasBeenSet().shinyapp;
+ }
+ function setShinyUser(user) {
+ validateShinyHasBeenSet().user = user;
+ }
+ function shinyForgetLastInputValue(name) {
+ validateShinyHasBeenSet().forgetLastInputValue(name);
+ }
+ async function shinyBindAll(scope) {
+ await validateShinyHasBeenSet().bindAll(scope);
+ }
+ function shinyUnbindAll(scope, includeSelf = false) {
+ validateShinyHasBeenSet().unbindAll(scope, includeSelf);
+ }
+ function shinyInitializeInputs(scope) {
+ validateShinyHasBeenSet().initializeInputs(scope);
+ }
+ async function shinyAppBindOutput(id, binding) {
+ await shinyShinyApp().bindOutput(id, binding);
+ }
+ function shinyAppUnbindOutput(id, binding) {
+ return shinyShinyApp().unbindOutput(id, binding);
+ }
+ function getShinyOnCustomMessage() {
+ return validateShinyHasBeenSet().oncustommessage;
+ }
+ var fileInputBinding;
+ function getFileInputBinding() {
+ return fileInputBinding;
+ }
+ function setFileInputBinding(fileInputBinding_) {
+ fileInputBinding = fileInputBinding_;
+ }
+ function getShinyCreateWebsocket() {
+ return validateShinyHasBeenSet().createSocket;
+ }
+
+ // srcts/src/time/debounce.ts
+ var Debouncer = class {
+ constructor(target, func, delayMs) {
+ this.target = target;
+ this.func = func;
+ this.delayMs = delayMs;
+ this.timerId = null;
+ this.args = null;
+ }
+ normalCall(...args) {
+ this.$clearTimer();
+ this.args = args;
+ this.timerId = setTimeout(() => {
+ if (this.timerId === null)
+ return;
+ this.$clearTimer();
+ this.$invoke();
+ }, this.delayMs);
+ }
+ immediateCall(...args) {
+ this.$clearTimer();
+ this.args = args;
+ this.$invoke();
+ }
+ isPending() {
+ return this.timerId !== null;
+ }
+ $clearTimer() {
+ if (this.timerId !== null) {
+ clearTimeout(this.timerId);
+ this.timerId = null;
+ }
+ }
+ $invoke() {
+ if (this.args && this.args.length > 0) {
+ this.func.apply(this.target, this.args);
+ } else {
+ this.func.apply(this.target);
+ }
+ this.args = null;
+ }
+ };
+ function debounce(threshold, func) {
+ let timerId = null;
+ return function thisFunc(...args) {
+ if (timerId !== null) {
+ clearTimeout(timerId);
+ timerId = null;
+ }
+ timerId = setTimeout(() => {
+ if (timerId === null)
+ return;
+ timerId = null;
+ func.apply(thisFunc, args);
+ }, threshold);
+ };
+ }
+
+ // srcts/src/time/invoke.ts
+ var Invoker = class {
+ constructor(target, func) {
+ this.target = target;
+ this.func = func;
+ }
+ normalCall(...args) {
+ this.func.apply(this.target, args);
+ }
+ immediateCall(...args) {
+ this.func.apply(this.target, args);
+ }
+ };
+
+ // srcts/src/time/throttle.ts
+ var Throttler = class {
+ constructor(target, func, delayMs) {
+ this.target = target;
+ this.func = func;
+ this.delayMs = delayMs;
+ this.timerId = null;
+ this.args = null;
+ }
+ normalCall(...args) {
+ this.args = args;
+ if (this.timerId === null) {
+ this.$invoke();
+ }
+ }
+ immediateCall(...args) {
+ this.$clearTimer();
+ this.args = args;
+ this.$invoke();
+ }
+ isPending() {
+ return this.args !== null;
+ }
+ $clearTimer() {
+ if (this.timerId !== null) {
+ clearTimeout(this.timerId);
+ this.timerId = null;
+ }
+ }
+ $invoke() {
+ if (this.args === null) {
+ return;
+ }
+ this.func.apply(this.target, this.args);
+ this.args = null;
+ this.timerId = setTimeout(() => {
+ if (this.timerId === null)
+ return;
+ this.$clearTimer();
+ if (this.isPending()) {
+ this.$invoke();
+ }
+ }, this.delayMs);
+ }
+ };
+
+ // srcts/src/shiny/sendImageSize.ts
+ var SendImageSize = class {
+ setImageSend(inputBatchSender, doSendImageSize) {
+ const sendImageSizeDebouncer = new Debouncer(null, doSendImageSize, 0);
+ this.regular = function() {
+ sendImageSizeDebouncer.normalCall();
+ };
+ inputBatchSender.lastChanceCallback.push(function() {
+ if (sendImageSizeDebouncer.isPending())
+ sendImageSizeDebouncer.immediateCall();
+ });
+ this.transitioned = debounce(200, this.regular);
+ return sendImageSizeDebouncer;
+ }
+ };
+ var sendImageSizeFns = new SendImageSize();
+
+ // srcts/src/shiny/singletons.ts
var import_jquery4 = __toESM(require_jquery());
+ var reSingleton = /([\s\S]*?)/;
+ var reHead = /]*)?>([\s\S]*?)<\/head>/;
+ var knownSingletons = {};
+ function renderHtml(html, el, where) {
+ const processed = processHtml(html);
+ addToHead(processed.head);
+ register(processed.singletons);
+ switch (where.toLowerCase()) {
+ case "replace":
+ (0, import_jquery4.default)(el).html(processed.html);
+ break;
+ case "beforebegin":
+ (0, import_jquery4.default)(el).before(processed.html);
+ break;
+ case "afterbegin":
+ (0, import_jquery4.default)(el).prepend(processed.html);
+ break;
+ case "beforeend":
+ (0, import_jquery4.default)(el).append(processed.html);
+ break;
+ case "afterend":
+ (0, import_jquery4.default)(el).after(processed.html);
+ break;
+ default:
+ throw new Error("Unknown where position: " + where);
+ }
+ return processed;
+ }
+ function register(s4) {
+ import_jquery4.default.extend(knownSingletons, s4);
+ }
+ function registerNames(s4) {
+ if (typeof s4 === "string") {
+ knownSingletons[s4] = true;
+ } else if (s4 instanceof Array) {
+ for (let i4 = 0; i4 < s4.length; i4++) {
+ knownSingletons[s4[i4]] = true;
+ }
+ }
+ }
+ function addToHead(head) {
+ if (head.length > 0) {
+ const tempDiv = (0, import_jquery4.default)("" + head + "
").get(0);
+ const $head = (0, import_jquery4.default)("head");
+ while (tempDiv.hasChildNodes()) {
+ $head.append(tempDiv.firstChild);
+ }
+ }
+ }
+ function processHtml(val) {
+ const newSingletons = {};
+ let newVal;
+ const findNewPayload = function(match, p1, sig, payload) {
+ if (knownSingletons[sig] || newSingletons[sig])
+ return "";
+ newSingletons[sig] = true;
+ return payload;
+ };
+ while (true) {
+ newVal = val.replace(reSingleton, findNewPayload);
+ if (val.length === newVal.length)
+ break;
+ val = newVal;
+ }
+ const heads = [];
+ const headAddPayload = function(match, payload) {
+ heads.push(payload);
+ return "";
+ };
+ while (true) {
+ newVal = val.replace(reHead, headAddPayload);
+ if (val.length === newVal.length)
+ break;
+ val = newVal;
+ }
+ return {
+ html: val,
+ head: heads.join("\n"),
+ singletons: newSingletons
+ };
+ }
+
+ // srcts/src/shiny/render.ts
+ async function renderContentAsync(el, content, where = "replace") {
+ if (where === "replace") {
+ shinyUnbindAll(el);
+ }
+ let html = "";
+ let dependencies = [];
+ if (content === null) {
+ html = "";
+ } else if (typeof content === "string") {
+ html = content;
+ } else if (typeof content === "object") {
+ html = content.html;
+ dependencies = content.deps || [];
+ }
+ await renderHtmlAsync(html, el, dependencies, where);
+ let scope = el;
+ if (where === "replace") {
+ shinyInitializeInputs(el);
+ await shinyBindAll(el);
+ } else {
+ const $parent = (0, import_jquery5.default)(el).parent();
+ if ($parent.length > 0) {
+ scope = $parent;
+ if (where === "beforeBegin" || where === "afterEnd") {
+ const $grandparent = $parent.parent();
+ if ($grandparent.length > 0)
+ scope = $grandparent;
+ }
+ }
+ shinyInitializeInputs(scope);
+ await shinyBindAll(scope);
+ }
+ }
+ function renderContent(el, content, where = "replace") {
+ if (where === "replace") {
+ shinyUnbindAll(el);
+ }
+ let html = "";
+ let dependencies = [];
+ if (content === null) {
+ html = "";
+ } else if (typeof content === "string") {
+ html = content;
+ } else if (typeof content === "object") {
+ html = content.html;
+ dependencies = content.deps || [];
+ }
+ renderHtml2(html, el, dependencies, where);
+ let scope = el;
+ if (where === "replace") {
+ shinyInitializeInputs(el);
+ return shinyBindAll(el);
+ } else {
+ const $parent = (0, import_jquery5.default)(el).parent();
+ if ($parent.length > 0) {
+ scope = $parent;
+ if (where === "beforeBegin" || where === "afterEnd") {
+ const $grandparent = $parent.parent();
+ if ($grandparent.length > 0)
+ scope = $grandparent;
+ }
+ }
+ shinyInitializeInputs(scope);
+ return shinyBindAll(scope);
+ }
+ }
+ async function renderHtmlAsync(html, el, dependencies, where = "replace") {
+ await renderDependenciesAsync(dependencies);
+ return renderHtml(html, el, where);
+ }
+ function renderHtml2(html, el, dependencies, where = "replace") {
+ renderDependencies(dependencies);
+ return renderHtml(html, el, where);
+ }
+ async function renderDependenciesAsync(dependencies) {
+ if (dependencies) {
+ for (const dep of dependencies) {
+ await renderDependencyAsync(dep);
+ }
+ }
+ }
+ function renderDependencies(dependencies) {
+ if (dependencies) {
+ for (const dep of dependencies) {
+ renderDependency(dep);
+ }
+ }
+ }
+ var htmlDependencies = {};
+ function registerDependency(name, version) {
+ htmlDependencies[name] = version;
+ }
+ function needsRestyle(dep) {
+ if (!dep.restyle) {
+ return false;
+ }
+ const names = Object.keys(htmlDependencies);
+ const idx = names.indexOf(dep.name);
+ if (idx === -1) {
+ return false;
+ }
+ return htmlDependencies[names[idx]] === dep.version;
+ }
+ function addStylesheetsAndRestyle(links) {
+ const $head = (0, import_jquery5.default)("head").first();
+ const refreshStyle = function(href, oldSheet) {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", href);
+ xhr.onload = function() {
+ const id = "shiny_restyle_" + href.split("?restyle")[0].replace(/\W/g, "_");
+ const oldStyle = $head.find("style#" + id);
+ const newStyle = (0, import_jquery5.default)("