|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +__all__ = ("input_submit_textarea", "update_submit_textarea") |
| 4 | + |
| 5 | +import copy |
| 6 | +from typing import Literal, Optional |
| 7 | + |
| 8 | +from htmltools import Tag, TagAttrValue, TagChild, css, div, span, tags |
| 9 | + |
| 10 | +from .._docstring import add_example |
| 11 | +from .._utils import drop_none |
| 12 | +from ..bookmark import restore_input |
| 13 | +from ..module import resolve_id |
| 14 | +from ..session import Session, require_active_session |
| 15 | +from ._html_deps_shinyverse import components_dependencies |
| 16 | +from ._input_task_button import input_task_button |
| 17 | +from ._utils import shiny_input_label |
| 18 | + |
| 19 | + |
| 20 | +@add_example() |
| 21 | +def input_submit_textarea( |
| 22 | + id: str, |
| 23 | + label: TagChild = None, |
| 24 | + *, |
| 25 | + placeholder: Optional[str] = None, |
| 26 | + value: str = "", |
| 27 | + width: str = "min(680px, 100%)", |
| 28 | + rows: int = 1, |
| 29 | + button: Optional[Tag] = None, |
| 30 | + toolbar: TagChild | TagAttrValue = None, |
| 31 | + submit_key: Literal["enter+modifier", "enter"] = "enter+modifier", |
| 32 | + **kwargs: TagAttrValue, |
| 33 | +) -> Tag: |
| 34 | + """ |
| 35 | + Create a textarea input control with explicit submission. |
| 36 | +
|
| 37 | + Creates a textarea input where users can enter multi-line text and submit |
| 38 | + their input using a dedicated button or keyboard shortcut. This control is |
| 39 | + ideal when you want to capture finalized input, rather than reacting to every |
| 40 | + keystroke, making it useful for chat boxes, comments, or other scenarios |
| 41 | + where users may compose and review their text before submitting. |
| 42 | +
|
| 43 | + Parameters |
| 44 | + ---------- |
| 45 | + id |
| 46 | + The input ID. |
| 47 | + label |
| 48 | + The label to display above the input control. If `None`, no label is displayed. |
| 49 | + placeholder |
| 50 | + The placeholder text to display when the input is empty. This can be used to |
| 51 | + provide a hint or example of the expected input. |
| 52 | + value |
| 53 | + The initial input text. Note that, unlike :func:`~shiny.ui.input_text_area`, |
| 54 | + this won't set a server-side value until the value is explicitly submitted. |
| 55 | + width |
| 56 | + Any valid CSS unit (e.g., `width="100%"`). |
| 57 | + rows |
| 58 | + The number of rows (i.e., height) of the textarea. This essentially sets the |
| 59 | + minimum height -- the textarea can grow taller as the user enters more text. |
| 60 | + button |
| 61 | + A :class:`~htmltools.Tag` element to use for the submit button. It's recommended |
| 62 | + that this be an :func:`~shiny.ui.input_task_button` since it will automatically |
| 63 | + provide a busy indicator (and disable) until the next flush occurs. Note also |
| 64 | + that if the submit button launches an :class:`~shiny.reactive.ExtendedTask`, |
| 65 | + this button can also be bound to the task (:func:`~shiny.ui.bind_task_button`) |
| 66 | + and/or manually updated for more accurate progress reporting |
| 67 | + (:func:`~shiny.ui.update_task_button`). |
| 68 | + toolbar |
| 69 | + UI elements to include alongside the submit button (e.g., help text, links, etc.). |
| 70 | + submit_key |
| 71 | + A string indicating what keyboard event should trigger the submit button. |
| 72 | + The default is `"enter+modifier"`, which requires the user to hold down |
| 73 | + Ctrl (or Cmd on Mac) before pressing Enter to submit. This helps prevent |
| 74 | + accidental submissions. To allow submission with just the Enter key, use |
| 75 | + `"enter"`. In this case, the user can still insert new lines using |
| 76 | + Shift+Enter or Alt+Enter. |
| 77 | + **kwargs |
| 78 | + Additional attributes to apply to the underlying `<textarea>` element |
| 79 | + (e.g., spellcheck, autocomplete, etc). |
| 80 | +
|
| 81 | + Returns |
| 82 | + ------- |
| 83 | + : |
| 84 | + A textarea input control that can be added to a UI definition. |
| 85 | +
|
| 86 | + Notes |
| 87 | + ------ |
| 88 | + ::: {.callout-note title="Server value"} |
| 89 | + A character string containing the user's text input. |
| 90 | +
|
| 91 | + **Important:** The server isn't sent a value until the user explicitly submits the |
| 92 | + input. This means that reading the input value results in a |
| 93 | + :class:`~shiny.types.SilentException` until the user actually submits input. After |
| 94 | + that, the server will only see updated values when the user submits the input again. |
| 95 | + For this reason, if you want to avoid the exception and return a value, check for |
| 96 | + the input ID using `if "input_id" in input` before reading the value. See the |
| 97 | + examples for a demonstration. |
| 98 | + ::: |
| 99 | +
|
| 100 | + See Also |
| 101 | + -------- |
| 102 | + * :func:`~shiny.ui.update_submit_textarea` |
| 103 | + * :func:`~shiny.ui.input_task_button` |
| 104 | + * :func:`~shiny.ui.input_text_area` |
| 105 | + """ |
| 106 | + resolved_id = resolve_id(id) |
| 107 | + value = restore_input(resolved_id, default=value) |
| 108 | + if not isinstance(value, str): |
| 109 | + raise TypeError("`value` must be a string") |
| 110 | + |
| 111 | + needs_modifier = submit_key == "enter+modifier" |
| 112 | + |
| 113 | + if button is None: |
| 114 | + button = input_task_button( |
| 115 | + id=f"{resolved_id}_submit", |
| 116 | + class_="btn-sm", |
| 117 | + label=span("\u23ce", class_="bslib-submit-key"), |
| 118 | + icon="Submit", |
| 119 | + label_busy=div( |
| 120 | + span("Processing...", class_="visually-hidden"), |
| 121 | + class_="spinner-border spinner-border-sm ms-2", |
| 122 | + role="status", |
| 123 | + ), |
| 124 | + icon_busy="Submit", |
| 125 | + title="Press Enter to Submit", |
| 126 | + aria_label="Press Enter to Submit", |
| 127 | + ) |
| 128 | + |
| 129 | + if not is_button_tag(button): |
| 130 | + raise TypeError("`button` must be a button tag") |
| 131 | + |
| 132 | + button2 = copy.copy(button) |
| 133 | + button2.add_class("bslib-submit-textarea-btn") |
| 134 | + |
| 135 | + return div( |
| 136 | + { |
| 137 | + "class": "bslib-input-submit-textarea shiny-input-container bslib-mb-spacing", |
| 138 | + "style": css(width=width), |
| 139 | + }, |
| 140 | + shiny_input_label(resolved_id, label), |
| 141 | + div( |
| 142 | + tags.textarea( |
| 143 | + value, |
| 144 | + {"class": "form-control", "style": css(width="100%")}, |
| 145 | + id=resolved_id, |
| 146 | + placeholder=placeholder, |
| 147 | + data_needs_modifier="" if needs_modifier else None, |
| 148 | + rows=rows, |
| 149 | + **kwargs, |
| 150 | + ), |
| 151 | + tags.footer( |
| 152 | + div(toolbar, class_="bslib-toolbar"), |
| 153 | + button2, |
| 154 | + ), |
| 155 | + class_="bslib-submit-textarea-container", |
| 156 | + ), |
| 157 | + components_dependencies(), |
| 158 | + ) |
| 159 | + |
| 160 | + |
| 161 | +def is_button_tag(x: object) -> bool: |
| 162 | + if not isinstance(x, Tag): |
| 163 | + return False |
| 164 | + return x.name == "button" or x.attrs.get("type") == "button" |
| 165 | + |
| 166 | + |
| 167 | +@add_example() |
| 168 | +def update_submit_textarea( |
| 169 | + id: str, |
| 170 | + *, |
| 171 | + value: Optional[str] = None, |
| 172 | + placeholder: Optional[str] = None, |
| 173 | + label: Optional[TagChild] = None, |
| 174 | + submit: bool = False, |
| 175 | + focus: bool = False, |
| 176 | + session: Optional[Session] = None, |
| 177 | +) -> None: |
| 178 | + """ |
| 179 | + Update a submit textarea input on the client. |
| 180 | +
|
| 181 | + Parameters |
| 182 | + ---------- |
| 183 | + id |
| 184 | + The input ID. |
| 185 | + value |
| 186 | + The value to set the user input to. |
| 187 | + placeholder |
| 188 | + The placeholder text for the user input. |
| 189 | + label |
| 190 | + The label for the input. |
| 191 | + submit |
| 192 | + Whether to automatically submit the text for the user. Requires `value`. |
| 193 | + focus |
| 194 | + Whether to move focus to the input element. Requires `value`. |
| 195 | + session |
| 196 | + A :class:`~shiny.Session` instance. If not provided, it is inferred via |
| 197 | + :func:`~shiny.session.get_current_session`. |
| 198 | +
|
| 199 | + See Also |
| 200 | + -------- |
| 201 | + * :func:`~shiny.ui.input_submit_textarea` |
| 202 | + """ |
| 203 | + if value is None and (submit or focus): |
| 204 | + raise ValueError( |
| 205 | + "An input `value` must be provided when `submit` or `focus` are `True`." |
| 206 | + ) |
| 207 | + |
| 208 | + session = require_active_session(session) |
| 209 | + |
| 210 | + msg = { |
| 211 | + "value": value, |
| 212 | + "placeholder": placeholder, |
| 213 | + "label": session._process_ui(label) if label is not None else None, |
| 214 | + "submit": submit, |
| 215 | + "focus": focus, |
| 216 | + } |
| 217 | + |
| 218 | + session.send_input_message(id, drop_none(msg)) |
0 commit comments