mirror of
https://github.com/gradio-app/gradio.git
synced 2024-12-27 02:30:17 +08:00
f5b710c919
* chore(deps): update dependency eslint to v9 * update deps + fix things * add changeset * fix preview * add changeset * lockfile * format * add changeset --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: pngwn <hello@pngwn.io> Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
342 lines
7.7 KiB
Svelte
342 lines
7.7 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
beforeUpdate,
|
|
afterUpdate,
|
|
createEventDispatcher,
|
|
tick
|
|
} from "svelte";
|
|
import { BlockTitle } from "@gradio/atoms";
|
|
import { Copy, Check } from "@gradio/icons";
|
|
import { fade } from "svelte/transition";
|
|
import type { SelectData } from "@gradio/utils";
|
|
|
|
export let value = "";
|
|
export let value_is_output = false;
|
|
export let lines = 1;
|
|
export let placeholder = "Type here...";
|
|
export let label: string;
|
|
export let info: string | undefined = undefined;
|
|
export let disabled = false;
|
|
export let show_label = true;
|
|
export let container = true;
|
|
export let max_lines: number;
|
|
export let type: "text" | "password" | "email" = "text";
|
|
export let show_copy_button = false;
|
|
export let rtl = false;
|
|
export let autofocus = false;
|
|
export let text_align: "left" | "right" | undefined = undefined;
|
|
export let autoscroll = true;
|
|
|
|
let el: HTMLTextAreaElement | HTMLInputElement;
|
|
let copied = false;
|
|
let timer: NodeJS.Timeout;
|
|
let can_scroll: boolean;
|
|
let previous_scroll_top = 0;
|
|
let user_has_scrolled_up = false;
|
|
|
|
$: value, el && lines !== max_lines && resize({ target: el });
|
|
|
|
$: if (value === null) value = "";
|
|
|
|
const dispatch = createEventDispatcher<{
|
|
change: string;
|
|
submit: undefined;
|
|
blur: undefined;
|
|
select: SelectData;
|
|
input: undefined;
|
|
focus: undefined;
|
|
}>();
|
|
|
|
beforeUpdate(() => {
|
|
can_scroll = el && el.offsetHeight + el.scrollTop > el.scrollHeight - 100;
|
|
});
|
|
|
|
const scroll = (): void => {
|
|
if (can_scroll && autoscroll && !user_has_scrolled_up) {
|
|
el.scrollTo(0, el.scrollHeight);
|
|
}
|
|
};
|
|
|
|
function handle_change(): void {
|
|
dispatch("change", value);
|
|
if (!value_is_output) {
|
|
dispatch("input");
|
|
}
|
|
}
|
|
afterUpdate(() => {
|
|
if (autofocus) {
|
|
el.focus();
|
|
}
|
|
if (can_scroll && autoscroll) {
|
|
scroll();
|
|
}
|
|
value_is_output = false;
|
|
});
|
|
$: value, handle_change();
|
|
|
|
async function handle_copy(): Promise<void> {
|
|
if ("clipboard" in navigator) {
|
|
await navigator.clipboard.writeText(value);
|
|
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
|
|
];
|
|
dispatch("select", { value: text.substring(...index), index: index });
|
|
}
|
|
|
|
async function handle_keypress(e: KeyboardEvent): Promise<void> {
|
|
await tick();
|
|
if (e.key === "Enter" && e.shiftKey && lines > 1) {
|
|
e.preventDefault();
|
|
dispatch("submit");
|
|
} else if (
|
|
e.key === "Enter" &&
|
|
!e.shiftKey &&
|
|
lines === 1 &&
|
|
max_lines >= 1
|
|
) {
|
|
e.preventDefault();
|
|
dispatch("submit");
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
async function resize(
|
|
event: Event | { target: HTMLTextAreaElement | HTMLInputElement }
|
|
): Promise<void> {
|
|
await tick();
|
|
if (lines === max_lines) return;
|
|
|
|
let max =
|
|
max_lines === undefined
|
|
? false
|
|
: max_lines === undefined // default
|
|
? 21 * 11
|
|
: 21 * (max_lines + 1);
|
|
let min = 21 * (lines + 1);
|
|
|
|
const target = event.target as HTMLTextAreaElement;
|
|
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`;
|
|
}
|
|
|
|
function text_area_resize(
|
|
_el: HTMLTextAreaElement,
|
|
_value: string
|
|
): any | undefined {
|
|
if (lines === max_lines) return;
|
|
_el.style.overflowY = "scroll";
|
|
_el.addEventListener("input", resize);
|
|
|
|
if (!_value.trim()) return;
|
|
resize({ target: _el });
|
|
|
|
return {
|
|
destroy: () => _el.removeEventListener("input", resize)
|
|
};
|
|
}
|
|
</script>
|
|
|
|
<!-- svelte-ignore a11y-autofocus -->
|
|
<label class:container>
|
|
<BlockTitle {show_label} {info}>{label}</BlockTitle>
|
|
|
|
{#if lines === 1 && max_lines === 1}
|
|
{#if type === "text"}
|
|
<input
|
|
data-testid="textbox"
|
|
type="text"
|
|
class="scroll-hide"
|
|
dir={rtl ? "rtl" : "ltr"}
|
|
bind:value
|
|
bind:this={el}
|
|
{placeholder}
|
|
{disabled}
|
|
{autofocus}
|
|
on:keypress={handle_keypress}
|
|
on:blur
|
|
on:select={handle_select}
|
|
on:focus
|
|
style={text_align ? "text-align: " + text_align : ""}
|
|
/>
|
|
{:else if type === "password"}
|
|
<input
|
|
data-testid="password"
|
|
type="password"
|
|
class="scroll-hide"
|
|
bind:value
|
|
bind:this={el}
|
|
{placeholder}
|
|
{disabled}
|
|
{autofocus}
|
|
on:keypress={handle_keypress}
|
|
on:blur
|
|
on:select={handle_select}
|
|
on:focus
|
|
autocomplete=""
|
|
/>
|
|
{:else if type === "email"}
|
|
<input
|
|
data-testid="textbox"
|
|
type="email"
|
|
class="scroll-hide"
|
|
bind:value
|
|
bind:this={el}
|
|
{placeholder}
|
|
{disabled}
|
|
{autofocus}
|
|
on:keypress={handle_keypress}
|
|
on:blur
|
|
on:select={handle_select}
|
|
on:focus
|
|
autocomplete="email"
|
|
/>
|
|
{/if}
|
|
{:else}
|
|
{#if show_label && show_copy_button}
|
|
{#if copied}
|
|
<button
|
|
in:fade={{ duration: 300 }}
|
|
aria-label="Copied"
|
|
aria-roledescription="Text copied"><Check /></button
|
|
>
|
|
{:else}
|
|
<button
|
|
on:click={handle_copy}
|
|
aria-label="Copy"
|
|
aria-roledescription="Copy text"><Copy /></button
|
|
>
|
|
{/if}
|
|
{/if}
|
|
<textarea
|
|
data-testid="textbox"
|
|
use:text_area_resize={value}
|
|
class="scroll-hide"
|
|
dir={rtl ? "rtl" : "ltr"}
|
|
bind:value
|
|
bind:this={el}
|
|
{placeholder}
|
|
rows={lines}
|
|
{disabled}
|
|
{autofocus}
|
|
on:keypress={handle_keypress}
|
|
on:blur
|
|
on:select={handle_select}
|
|
on:focus
|
|
on:scroll={handle_scroll}
|
|
style={text_align ? "text-align: " + text_align : ""}
|
|
/>
|
|
{/if}
|
|
</label>
|
|
|
|
<style>
|
|
label {
|
|
display: block;
|
|
width: 100%;
|
|
}
|
|
|
|
input,
|
|
textarea {
|
|
display: block;
|
|
position: relative;
|
|
outline: none !important;
|
|
box-shadow: var(--input-shadow);
|
|
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;
|
|
}
|
|
label:not(.container),
|
|
label:not(.container) > input,
|
|
label:not(.container) > textarea {
|
|
height: 100%;
|
|
}
|
|
.container > input,
|
|
.container > textarea {
|
|
border: var(--input-border-width) solid var(--input-border-color);
|
|
border-radius: var(--input-radius);
|
|
}
|
|
input:disabled,
|
|
textarea:disabled {
|
|
-webkit-text-fill-color: var(--body-text-color);
|
|
-webkit-opacity: 1;
|
|
opacity: 1;
|
|
}
|
|
|
|
input:focus,
|
|
textarea:focus {
|
|
box-shadow: var(--input-shadow-focus);
|
|
border-color: var(--input-border-color-focus);
|
|
}
|
|
|
|
input::placeholder,
|
|
textarea::placeholder {
|
|
color: var(--input-placeholder-color);
|
|
}
|
|
button {
|
|
display: flex;
|
|
position: absolute;
|
|
top: var(--block-label-margin);
|
|
right: var(--block-label-margin);
|
|
align-items: center;
|
|
box-shadow: var(--shadow-drop);
|
|
border: 1px solid var(--color-border-primary);
|
|
border-top: none;
|
|
border-right: none;
|
|
border-radius: var(--block-label-right-radius);
|
|
background: var(--block-label-background-fill);
|
|
padding: 5px;
|
|
width: 22px;
|
|
height: 22px;
|
|
overflow: hidden;
|
|
color: var(--block-label-color);
|
|
font: var(--font-sans);
|
|
font-size: var(--button-small-text-size);
|
|
}
|
|
</style>
|