gradio/js/textbox/shared/Textbox.svelte
renovate[bot] f5b710c919
chore(deps): update dependency eslint to v9 (#8121)
* 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>
2024-05-03 09:57:08 +01:00

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>