mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-14 09:28:02 -05:00
feat(textAreaInput): Add an autoresize option (#4210)
* feat(textAreaInput): Add an autoresize option * `devtools::document()` (GitHub Actions) * `yarn build` (GitHub Actions) * Update NEWS.md * Fix broken CSS selector. Rules aren't being applied correctly in PyShiny either... * Put shiny input class on container (to mirror what PyShiny does) * Refactor autoresize logic * Reduce diff size --------- Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
This commit is contained in:
14
NEWS.md
14
NEWS.md
@@ -1,6 +1,14 @@
|
||||
# shiny (development version)
|
||||
|
||||
## New features and improvements
|
||||
## New features
|
||||
|
||||
* `textInput()`, `textAreaInput()`, `numericInput()` and `passwordInput()` all gain an `updateOn` option. `updateOn = "change"` is the default and previous behavior, where the input value updates immediately whenever the value changes. With `updateOn = "blur"`, the input value will update only when the text input loses focus or when the user presses Enter (or Cmd/Ctrl + Enter for `textAreaInput()`). (#4183)
|
||||
|
||||
* `textAreaInput()` gains a `autoresize` option, which automatically resizes the text area to fit its content. (#4210)
|
||||
|
||||
## Improvements
|
||||
|
||||
* When auto-reload is enabled, Shiny now reloads the entire app when support files, like Shiny modules, additional script files, or web assets, change. To enable auto-reload, call `devmode(TRUE)` to enable Shiny's developer mode, or set `options(shiny.autoreload = TRUE)` to specifically enable auto-reload. You can choose which files are watched for changes with the `shiny.autoreload.pattern` option. (#4184)
|
||||
|
||||
* When busy indicators are enabled (i.e., `useBusyIndicators()`), Shiny now:
|
||||
* Shows a spinner on recalculating htmlwidgets that have previously rendered an error (including `req()` and `validate()`). (#4172)
|
||||
@@ -11,10 +19,6 @@
|
||||
|
||||
* Shiny's Typescript assets are now compiled to ES2021 instead of ES5. (#4066)
|
||||
|
||||
* `textInput()`, `textAreaInput()`, `numericInput()` and `passwordInput()` all gain an `updateOn` option. `updateOn = "change"` is the default and previous behavior, where the input value updates immediately whenever the value changes. With `updateOn = "blur"`, the input value will update only when the text input loses focus or when the user presses Enter (or Cmd/Ctrl + Enter for `textAreaInput()`). (#4183)
|
||||
|
||||
* When auto-reload is enabled, Shiny now reloads the entire app when support files, like Shiny modules, additional script files, or web assets, change. To enable auto-reload, call `devmode(TRUE)` to enable Shiny's developer mode, or set `options(shiny.autoreload = TRUE)` to specifically enable auto-reload. You can choose which files are watched for changes with the `shiny.autoreload.pattern` option. (#4184)
|
||||
|
||||
## Bug fixes
|
||||
|
||||
* Fixed a bug with modals where calling `removeModal()` too quickly after `showModal()` would fail to remove the modal if the remove modal message was received while the modal was in the process of being revealed. (#4173)
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#' @param resize Which directions the textarea box can be resized. Can be one of
|
||||
#' `"both"`, `"none"`, `"vertical"`, and `"horizontal"`. The default, `NULL`,
|
||||
#' will use the client browser's default setting for resizing textareas.
|
||||
#' @param autoresize If `TRUE`, the textarea will automatically resize to fit
|
||||
#' the input text.
|
||||
#' @return A textarea input control that can be added to a UI definition.
|
||||
#'
|
||||
#' @family input elements
|
||||
@@ -52,6 +54,7 @@ textAreaInput <- function(
|
||||
placeholder = NULL,
|
||||
resize = NULL,
|
||||
...,
|
||||
autoresize = FALSE,
|
||||
updateOn = c("change", "blur")
|
||||
) {
|
||||
rlang::check_dots_empty()
|
||||
@@ -63,22 +66,27 @@ textAreaInput <- function(
|
||||
resize <- match.arg(resize, c("both", "none", "vertical", "horizontal"))
|
||||
}
|
||||
|
||||
style <- css(
|
||||
# The width is specified on the parent div.
|
||||
width = if (!is.null(width)) "100%",
|
||||
height = validateCssUnit(height),
|
||||
resize = resize
|
||||
)
|
||||
classes <- "form-control"
|
||||
if (autoresize) {
|
||||
classes <- c(classes, "textarea-autoresize")
|
||||
if (is.null(rows)) {
|
||||
rows <- 1
|
||||
}
|
||||
}
|
||||
|
||||
div(
|
||||
class = "form-group shiny-input-container",
|
||||
class = "shiny-input-textarea form-group shiny-input-container",
|
||||
style = css(width = validateCssUnit(width)),
|
||||
shinyInputLabel(inputId, label),
|
||||
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
|
||||
tags$textarea(
|
||||
id = inputId,
|
||||
class = "shiny-input-textarea form-control",
|
||||
class = classes,
|
||||
placeholder = placeholder,
|
||||
style = style,
|
||||
style = css(
|
||||
width = if (!is.null(width)) "100%",
|
||||
height = validateCssUnit(height),
|
||||
resize = resize
|
||||
),
|
||||
rows = rows,
|
||||
cols = cols,
|
||||
`data-update-on` = updateOn,
|
||||
|
||||
@@ -26,11 +26,20 @@
|
||||
if (!member.has(obj))
|
||||
throw TypeError("Cannot " + msg);
|
||||
};
|
||||
var __privateGet = (obj, member, getter) => {
|
||||
__accessCheck(obj, member, "read from private field");
|
||||
return getter ? getter.call(obj) : member.get(obj);
|
||||
};
|
||||
var __privateAdd = (obj, member, value) => {
|
||||
if (member.has(obj))
|
||||
throw TypeError("Cannot add the same private member more than once");
|
||||
member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
||||
};
|
||||
var __privateSet = (obj, member, value, setter) => {
|
||||
__accessCheck(obj, member, "write to private field");
|
||||
setter ? setter.call(obj, value) : member.set(obj, value);
|
||||
return value;
|
||||
};
|
||||
var __privateMethod = (obj, member, method) => {
|
||||
__accessCheck(obj, member, "access private method");
|
||||
return method;
|
||||
@@ -2191,11 +2200,50 @@
|
||||
|
||||
// srcts/src/bindings/input/textarea.ts
|
||||
var import_jquery20 = __toESM(require_jquery());
|
||||
var intersectObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
updateHeight(entry.target);
|
||||
}
|
||||
});
|
||||
});
|
||||
var _inputHandler;
|
||||
var TextareaInputBinding = class extends TextInputBinding {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
__privateAdd(this, _inputHandler, null);
|
||||
}
|
||||
find(scope) {
|
||||
return (0, import_jquery20.default)(scope).find("textarea");
|
||||
}
|
||||
initialize(el) {
|
||||
super.initialize(el);
|
||||
updateHeight(el);
|
||||
}
|
||||
subscribe(el, callback) {
|
||||
super.subscribe(el, callback);
|
||||
__privateSet(this, _inputHandler, (e4) => updateHeight(e4.target));
|
||||
el.addEventListener("input", __privateGet(this, _inputHandler));
|
||||
intersectObserver.observe(el);
|
||||
}
|
||||
unsubscribe(el) {
|
||||
super.unsubscribe(el);
|
||||
if (__privateGet(this, _inputHandler))
|
||||
el.removeEventListener("input", __privateGet(this, _inputHandler));
|
||||
intersectObserver.unobserve(el);
|
||||
}
|
||||
};
|
||||
_inputHandler = new WeakMap();
|
||||
function updateHeight(el) {
|
||||
if (!el.classList.contains("textarea-autoresize")) {
|
||||
return;
|
||||
}
|
||||
if (el.scrollHeight == 0) {
|
||||
return;
|
||||
}
|
||||
el.style.height = "auto";
|
||||
el.style.height = el.scrollHeight + "px";
|
||||
}
|
||||
|
||||
// srcts/src/bindings/input/index.ts
|
||||
function initInputBindings() {
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
inst/www/shared/shiny.min.css
vendored
2
inst/www/shared/shiny.min.css
vendored
File diff suppressed because one or more lines are too long
40
inst/www/shared/shiny.min.js
vendored
40
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -384,6 +384,14 @@ html.autoreload-enabled #shiny-disconnected-overlay.reloading {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Styling for textAreaInput(autoresize=TRUE) */
|
||||
textarea.textarea-autoresize.form-control {
|
||||
padding: 5px 8px;
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
#shiny-notification-panel {
|
||||
position: fixed;
|
||||
|
||||
@@ -15,6 +15,7 @@ textAreaInput(
|
||||
placeholder = NULL,
|
||||
resize = NULL,
|
||||
...,
|
||||
autoresize = FALSE,
|
||||
updateOn = c("change", "blur")
|
||||
)
|
||||
}
|
||||
@@ -52,6 +53,9 @@ will use the client browser's default setting for resizing textareas.}
|
||||
\item{...}{Ignored, included to require named arguments and for future
|
||||
feature expansion.}
|
||||
|
||||
\item{autoresize}{If \code{TRUE}, the textarea will automatically resize to fit
|
||||
the input text.}
|
||||
|
||||
\item{updateOn}{A character vector specifying when the input should be
|
||||
updated. Options are \code{"change"} (default) and \code{"blur"}. Use \code{"change"} to
|
||||
update the input immediately whenever the value changes. Use \code{"blur"}to
|
||||
|
||||
@@ -2,11 +2,53 @@ import $ from "jquery";
|
||||
|
||||
import { TextInputBinding } from "./text";
|
||||
|
||||
// When a textarea becomes visible, update the height
|
||||
const intersectObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
updateHeight(entry.target as HTMLInputElement);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
class TextareaInputBinding extends TextInputBinding {
|
||||
#inputHandler: EventListener | null = null;
|
||||
|
||||
find(scope: HTMLElement): JQuery<HTMLElement> {
|
||||
// Inputs now also have the .shiny-input-textarea class
|
||||
return $(scope).find("textarea");
|
||||
}
|
||||
|
||||
initialize(el: HTMLInputElement): void {
|
||||
super.initialize(el);
|
||||
updateHeight(el);
|
||||
}
|
||||
|
||||
subscribe(el: HTMLInputElement, callback: (x: boolean) => void): void {
|
||||
super.subscribe(el, callback);
|
||||
|
||||
this.#inputHandler = (e) => updateHeight(e.target as HTMLInputElement);
|
||||
el.addEventListener("input", this.#inputHandler);
|
||||
intersectObserver.observe(el);
|
||||
}
|
||||
|
||||
unsubscribe(el: HTMLInputElement): void {
|
||||
super.unsubscribe(el);
|
||||
|
||||
if (this.#inputHandler) el.removeEventListener("input", this.#inputHandler);
|
||||
intersectObserver.unobserve(el);
|
||||
}
|
||||
}
|
||||
|
||||
function updateHeight(el: HTMLInputElement) {
|
||||
if (!el.classList.contains("textarea-autoresize")) {
|
||||
return;
|
||||
}
|
||||
if (el.scrollHeight == 0) {
|
||||
return;
|
||||
}
|
||||
el.style.height = "auto";
|
||||
el.style.height = el.scrollHeight + "px";
|
||||
}
|
||||
|
||||
export { TextareaInputBinding };
|
||||
|
||||
4
srcts/types/src/bindings/input/textarea.d.ts
vendored
4
srcts/types/src/bindings/input/textarea.d.ts
vendored
@@ -1,5 +1,9 @@
|
||||
import { TextInputBinding } from "./text";
|
||||
declare class TextareaInputBinding extends TextInputBinding {
|
||||
#private;
|
||||
find(scope: HTMLElement): JQuery<HTMLElement>;
|
||||
initialize(el: HTMLInputElement): void;
|
||||
subscribe(el: HTMLInputElement, callback: (x: boolean) => void): void;
|
||||
unsubscribe(el: HTMLInputElement): void;
|
||||
}
|
||||
export { TextareaInputBinding };
|
||||
|
||||
Reference in New Issue
Block a user