Files
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

585 lines
14 KiB
Svelte

<script lang="ts">
import { tick } from "svelte";
import { BlockTitle, IconButton, IconButtonWrapper } from "@gradio/atoms";
import { Copy, Check, Send, Square } from "@gradio/icons";
import type {
SelectData,
CopyData,
CustomButton as CustomButtonType
} from "@gradio/utils";
import type { InputHTMLAttributes } from "../types";
let {
value = $bindable(""),
value_is_output = $bindable(false),
lines = 1,
placeholder = "",
label,
info = undefined,
disabled = false,
show_label = true,
container = true,
max_lines = undefined,
type = "text",
buttons = null,
oncustombuttonclick = null,
submit_btn = null,
stop_btn = null,
rtl = false,
autofocus = false,
text_align = undefined,
autoscroll = true,
max_length = undefined,
html_attributes = null,
validation_error = undefined,
onchange,
onsubmit,
onstop,
onblur,
onselect,
oninput,
onfocus,
oncopy
}: {
value?: string;
value_is_output?: boolean;
lines?: number;
placeholder?: string;
label: string;
info?: string | undefined;
disabled?: boolean;
show_label?: boolean;
container?: boolean;
max_lines?: number | undefined;
type?: "text" | "password" | "email";
buttons?: (string | CustomButtonType)[] | null;
oncustombuttonclick?: ((id: number) => void) | null;
submit_btn?: string | boolean | null;
stop_btn?: string | boolean | null;
rtl?: boolean;
autofocus?: boolean;
text_align?: "left" | "right" | undefined;
autoscroll?: boolean;
max_length?: number | undefined;
html_attributes?: InputHTMLAttributes | null;
validation_error?: string | null | undefined;
onchange?: (value: string) => void;
onsubmit?: () => void;
onstop?: () => void;
onblur?: () => void;
onselect?: (data: SelectData) => void;
oninput?: (value: string) => void;
onfocus?: () => void;
oncopy?: (data: CopyData) => void;
} = $props();
// svelte-ignore non_reactive_update
let el: HTMLTextAreaElement | HTMLInputElement;
let copied = $state(false);
let timer: NodeJS.Timeout;
let can_scroll = $state(false);
let previous_scroll_top = $state(0);
let user_has_scrolled_up = $state(false);
let _max_lines = $state(1);
// svelte-ignore state_referenced_locally
const show_textbox_border = !submit_btn;
$effect(() => {
if (max_lines === undefined || max_lines === null) {
if (type === "text") {
_max_lines = Math.max(lines, 20);
} else {
_max_lines = 1;
}
} else {
_max_lines = Math.max(max_lines, lines);
}
});
$effect(() => {
value;
validation_error;
if (el && lines !== _max_lines && _max_lines > 1) {
resize({ target: el });
}
});
$effect(() => {
if (value === null) value = "";
});
$effect.pre(() => {
if (
!user_has_scrolled_up &&
el &&
el.offsetHeight + el.scrollTop > el.scrollHeight - 100
) {
can_scroll = true;
}
});
const scroll = (): void => {
if (can_scroll && autoscroll && !user_has_scrolled_up) {
el.scrollTo(0, el.scrollHeight);
}
};
async function handle_change(): Promise<void> {
await tick();
onchange?.(value);
}
$effect(() => {
if (autofocus && el) {
el.focus();
}
if (can_scroll && autoscroll) {
scroll();
}
value_is_output = false;
});
$effect(() => {
value;
handle_change();
});
async function handle_copy(): Promise<void> {
if ("clipboard" in navigator) {
try {
await navigator.clipboard.writeText(value);
oncopy?.({ value: value });
} catch (e) {
console.error("COPYING CLIPBOARD FAILED", e);
}
copy_feedback();
}
}
function copy_feedback(): void {
copied = true;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
copied = false;
}, 1000);
}
function handle_select(event: Event): void {
const target: HTMLTextAreaElement | HTMLInputElement = event.target as
| HTMLTextAreaElement
| HTMLInputElement;
const text = target.value;
const index: [number, number] = [
target.selectionStart as number,
target.selectionEnd as number
];
onselect?.({ value: text.substring(...index), index: index });
}
async function handle_keypress(e: KeyboardEvent): Promise<void> {
if (e.key === "Enter" && e.shiftKey && lines > 1) {
e.preventDefault();
await tick();
onsubmit?.();
} else if (
e.key === "Enter" &&
!e.shiftKey &&
lines === 1 &&
_max_lines >= 1
) {
e.preventDefault();
await tick();
onsubmit?.();
}
await tick();
oninput?.(value);
}
function handle_scroll(event: Event): void {
const target = event.target as HTMLElement;
const current_scroll_top = target.scrollTop;
if (current_scroll_top < previous_scroll_top) {
user_has_scrolled_up = true;
}
previous_scroll_top = current_scroll_top;
const max_scroll_top = target.scrollHeight - target.clientHeight;
const user_has_scrolled_to_bottom = current_scroll_top >= max_scroll_top;
if (user_has_scrolled_to_bottom) {
user_has_scrolled_up = false;
}
}
function handle_stop(): void {
onstop?.();
}
function handle_submit(): void {
onsubmit?.();
}
async function resize(
event: Event | { target: HTMLTextAreaElement | HTMLInputElement }
): Promise<void> {
await tick();
if (lines === _max_lines) return;
const target = event.target as HTMLTextAreaElement;
const computed_styles = window.getComputedStyle(target);
const padding_top = parseFloat(computed_styles.paddingTop);
const padding_bottom = parseFloat(computed_styles.paddingBottom);
const line_height = parseFloat(computed_styles.lineHeight);
let max =
_max_lines === undefined
? false
: padding_top + padding_bottom + line_height * _max_lines;
let min = padding_top + padding_bottom + lines * line_height;
target.style.height = "1px";
let scroll_height;
if (max && target.scrollHeight > max) {
scroll_height = max;
} else if (target.scrollHeight < min) {
scroll_height = min;
} else {
scroll_height = target.scrollHeight;
}
target.style.height = `${scroll_height}px`;
update_scrollbar_visibility(target);
}
function update_scrollbar_visibility(textarea: HTMLTextAreaElement): void {
// Using "auto" scroll does not work, as the scrollbar is visible even
// when the content is about the same height as the textarea height. So
// here, we add the scrollbar if the content is longer than a threshold
// of 1 line height beyond the textarea height.
const content_height = textarea.scrollHeight;
const visible_height = textarea.clientHeight;
const line_height = parseFloat(
window.getComputedStyle(textarea).lineHeight
);
if (content_height > visible_height + line_height) {
textarea.style.overflowY = "scroll";
} else {
textarea.style.overflowY = "hidden";
}
}
function text_area_resize(
_el: HTMLTextAreaElement,
_value: string
): any | undefined {
if (lines === _max_lines || (lines === 1 && _max_lines === 1)) return;
_el.addEventListener("input", resize);
if (_value.trim()) {
_el.style.overflowY = "scroll";
resize({ target: _el });
}
return {
destroy: () => _el.removeEventListener("input", resize)
};
}
</script>
<label class:container class:show_textbox_border>
{#if show_label && buttons && buttons.length > 0}
<IconButtonWrapper {buttons} on_custom_button_click={oncustombuttonclick}>
{#if buttons.some((btn) => typeof btn === "string" && btn === "copy")}
<IconButton
Icon={copied ? Check : Copy}
onclick={handle_copy}
label={copied ? "Copied" : "Copy"}
/>
{/if}
</IconButtonWrapper>
{/if}
<BlockTitle show_label={validation_error ? true : show_label} {info}
>{label}
{#if validation_error}
<div class="validation-error">{validation_error}</div>
{/if}
</BlockTitle>
<div class="input-container">
{#if lines === 1 && _max_lines === 1}
{#if type === "text"}
<!-- svelte-ignore a11y_autofocus -->
<input
data-testid="textbox"
type="text"
class="scroll-hide"
dir={rtl ? "rtl" : "ltr"}
bind:value
bind:this={el}
{placeholder}
{disabled}
{autofocus}
maxlength={max_length}
onkeypress={handle_keypress}
onblur={() => onblur?.()}
onselect={handle_select}
onfocus={() => onfocus?.()}
class:validation-error={validation_error}
style={text_align ? "text-align: " + text_align : ""}
autocapitalize={html_attributes?.autocapitalize}
autocorrect={html_attributes?.autocorrect}
spellcheck={html_attributes?.spellcheck}
autocomplete={html_attributes?.autocomplete}
tabindex={html_attributes?.tabindex}
enterkeyhint={html_attributes?.enterkeyhint}
lang={html_attributes?.lang}
/>
{:else if type === "password"}
<!-- svelte-ignore a11y_autofocus -->
<input
data-testid="password"
type="password"
class="scroll-hide"
bind:value
bind:this={el}
{placeholder}
{disabled}
{autofocus}
maxlength={max_length}
onkeypress={handle_keypress}
onblur={() => onblur?.()}
onselect={handle_select}
onfocus={() => onfocus?.()}
class:validation-error={validation_error}
autocomplete=""
autocapitalize={html_attributes?.autocapitalize}
autocorrect={html_attributes?.autocorrect}
spellcheck={html_attributes?.spellcheck}
tabindex={html_attributes?.tabindex}
enterkeyhint={html_attributes?.enterkeyhint}
lang={html_attributes?.lang}
/>
{:else if type === "email"}
<!-- svelte-ignore a11y_autofocus -->
<input
data-testid="textbox"
type="email"
class="scroll-hide"
bind:value
bind:this={el}
{placeholder}
{disabled}
{autofocus}
maxlength={max_length}
onkeypress={handle_keypress}
onblur={() => onblur?.()}
onselect={handle_select}
onfocus={() => onfocus?.()}
class:validation-error={validation_error}
autocomplete="email"
autocapitalize={html_attributes?.autocapitalize}
autocorrect={html_attributes?.autocorrect}
spellcheck={html_attributes?.spellcheck}
tabindex={html_attributes?.tabindex}
enterkeyhint={html_attributes?.enterkeyhint}
lang={html_attributes?.lang}
/>
{/if}
{:else}
<!-- svelte-ignore a11y_autofocus -->
<textarea
data-testid="textbox"
use:text_area_resize={value}
dir={rtl ? "rtl" : "ltr"}
class:no-label={!show_label && (submit_btn || stop_btn)}
bind:value
bind:this={el}
{placeholder}
rows={lines}
{disabled}
{autofocus}
maxlength={max_length}
onkeypress={handle_keypress}
onblur={() => onblur?.()}
onselect={handle_select}
onfocus={() => onfocus?.()}
onscroll={handle_scroll}
class:validation-error={validation_error}
style={text_align ? "text-align: " + text_align : ""}
autocapitalize={html_attributes?.autocapitalize}
autocorrect={html_attributes?.autocorrect}
spellcheck={html_attributes?.spellcheck}
autocomplete={html_attributes?.autocomplete}
tabindex={html_attributes?.tabindex}
enterkeyhint={html_attributes?.enterkeyhint}
lang={html_attributes?.lang}
></textarea>
{/if}
{#if submit_btn}
<button
class="submit-button"
class:padded-button={submit_btn !== true}
onclick={handle_submit}
data-testid="submit-button"
>
{#if submit_btn === true}
<Send />
{:else}
{submit_btn}
{/if}
</button>
{/if}
{#if stop_btn}
<button
class="stop-button"
class:padded-button={stop_btn !== true}
onclick={handle_stop}
data-testid="stop-button"
>
{#if stop_btn === true}
<Square fill="none" stroke_width={2.5} />
{:else}
{stop_btn}
{/if}
</button>
{/if}
</div>
</label>
<style>
label {
display: block;
width: 100%;
}
input[type="text"],
input[type="password"],
input[type="email"],
textarea {
flex-grow: 1;
outline: none !important;
margin-top: 0px;
margin-bottom: 0px;
resize: none;
z-index: 1;
display: block;
position: relative;
outline: none !important;
background: var(--input-background-fill);
padding: var(--input-padding);
width: 100%;
color: var(--body-text-color);
font-weight: var(--input-text-weight);
font-size: var(--input-text-size);
line-height: var(--line-sm);
border: none;
}
textarea.no-label {
padding-top: 5px;
padding-bottom: 5px;
}
label.show_textbox_border input,
label.show_textbox_border textarea {
box-shadow: var(--input-shadow);
}
label:not(.container),
label:not(.container) input,
label:not(.container) textarea {
height: 100%;
}
label.container.show_textbox_border input,
label.container.show_textbox_border textarea {
border: var(--input-border-width) solid var(--input-border-color);
border-radius: var(--input-radius);
}
input:disabled,
textarea:disabled {
-webkit-opacity: 1;
opacity: 1;
}
label.container.show_textbox_border input:focus,
label.container.show_textbox_border textarea:focus {
box-shadow: var(--input-shadow-focus);
border-color: var(--input-border-color-focus);
background: var(--input-background-fill-focus);
}
input::placeholder,
textarea::placeholder {
color: var(--input-placeholder-color);
}
/* Same submit button style as MultimodalTextbox for the consistent UI */
.input-container {
display: flex;
position: relative;
align-items: flex-end;
}
.submit-button,
.stop-button {
border: none;
text-align: center;
text-decoration: none;
font-size: 14px;
cursor: pointer;
border-radius: 15px;
min-width: 30px;
height: 30px;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: var(--layer-1);
}
.stop-button,
.submit-button {
background: var(--button-secondary-background-fill);
color: var(--button-secondary-text-color);
}
.stop-button:hover,
.submit-button:hover {
background: var(--button-secondary-background-fill-hover);
}
.stop-button:disabled,
.submit-button:disabled {
background: var(--button-secondary-background-fill);
cursor: pointer;
}
.stop-button:active,
.submit-button:active {
box-shadow: var(--button-shadow-active);
}
.submit-button :global(svg) {
height: 22px;
width: 22px;
}
.stop-button :global(svg) {
height: 16px;
width: 16px;
}
.padded-button {
padding: 0 10px;
}
div.validation-error {
color: var(--error-icon-color);
font-size: var(--font-sans);
margin-top: var(--spacing-sm);
font-weight: var(--weight-semibold);
}
label.container input.validation-error,
label.container textarea.validation-error {
border-color: transparent !important;
box-shadow:
0 0 3px 1px var(--error-icon-color),
var(--shadow-inset) !important;
}
</style>