mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-11-09 21:30:38 +08:00
216 lines
6.1 KiB
Svelte
216 lines
6.1 KiB
Svelte
<script lang="ts">
|
|
import type {
|
|
ClipboardEventHandler,
|
|
FormEventHandler,
|
|
} from "svelte/elements";
|
|
|
|
import { onMount, tick } from "svelte";
|
|
|
|
import * as utils from "@app/lib/utils";
|
|
|
|
interface Props {
|
|
draggingOver?: boolean;
|
|
borderVariant?: "float" | "ghost";
|
|
onpaste?: ClipboardEventHandler<HTMLTextAreaElement>;
|
|
focus?: boolean;
|
|
oninput?: FormEventHandler<HTMLTextAreaElement>;
|
|
onkeypress?: FormEventHandler<HTMLTextAreaElement>;
|
|
placeholder?: string;
|
|
selectionEnd?: number;
|
|
selectionStart?: number;
|
|
size?: "grow" | "resizable" | "fixed-height";
|
|
styleMinHeight?: string;
|
|
styleAlignItems?: string;
|
|
stylePadding?: string;
|
|
submit: () => Promise<void>;
|
|
value?: string;
|
|
}
|
|
|
|
/* eslint-disable prefer-const */
|
|
let {
|
|
draggingOver,
|
|
borderVariant = "float",
|
|
focus = false,
|
|
onpaste,
|
|
oninput,
|
|
onkeypress,
|
|
placeholder = undefined,
|
|
// Defaulting selectionStart and selectionEnd to 0, since no full support yet.
|
|
selectionEnd = $bindable(0),
|
|
selectionStart = $bindable(0),
|
|
size = "grow",
|
|
styleAlignItems = undefined,
|
|
styleMinHeight = undefined,
|
|
stylePadding = "0.75rem",
|
|
submit,
|
|
value = $bindable(undefined),
|
|
}: Props = $props();
|
|
/* eslint-enable prefer-const */
|
|
|
|
const borderColors: Record<NonNullable<Props["borderVariant"]>, string> = {
|
|
ghost: "var(--color-border-subtle)",
|
|
float: "var(--color-border-subtle)",
|
|
};
|
|
|
|
let textareaElement: HTMLTextAreaElement | undefined = $state(undefined);
|
|
let focussed = $state(false);
|
|
|
|
onMount(() => {
|
|
if (textareaElement) {
|
|
// The selectionchange event listener doesn't modify the selection on Enter.
|
|
textareaElement.addEventListener("keydown", (event: KeyboardEvent) => {
|
|
if (event.key === "Enter") {
|
|
selectionStart += 1;
|
|
selectionEnd += 1;
|
|
}
|
|
});
|
|
textareaElement.addEventListener("selectionchange", (event: Event) => {
|
|
if (
|
|
event.target &&
|
|
"selectionStart" in event.target &&
|
|
"selectionEnd" in event.target
|
|
) {
|
|
selectionStart = event.target.selectionStart as number;
|
|
selectionEnd = event.target.selectionEnd as number;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// We either auto-grow the textarea, or allow the user to resize it. These
|
|
// options are mutually exclusive because a user resized textarea would
|
|
// automatically shrink upon text input otherwise.
|
|
$effect(() => {
|
|
if (textareaElement && size === "grow") {
|
|
// React to changes to the textarea content.
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
value;
|
|
|
|
// Reset height to 0px on every value change so that the textarea
|
|
// immediately shrinks when all text is deleted.
|
|
textareaElement.style.height = `0px`;
|
|
textareaElement.style.height = `${textareaElement.scrollHeight}px`;
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
if (textareaElement && focus) {
|
|
textareaElement.focus();
|
|
focus = false;
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
void tick().then(() => {
|
|
if (textareaElement && focus) {
|
|
textareaElement.setSelectionRange(selectionStart, selectionEnd);
|
|
textareaElement.focus();
|
|
}
|
|
});
|
|
});
|
|
|
|
function handleKeydown(event: KeyboardEvent) {
|
|
event.stopPropagation();
|
|
const auxiliarKey = utils.isMac() ? event.metaKey : event.ctrlKey;
|
|
if (auxiliarKey && event.key === "Enter") {
|
|
void submit();
|
|
}
|
|
if (event.key === "Escape") {
|
|
textareaElement?.blur();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
textarea {
|
|
background-color: transparent;
|
|
border: 0;
|
|
color: var(--color-text-secondary);
|
|
font-family: inherit;
|
|
height: 100%;
|
|
width: 100%;
|
|
min-height: 6.375rem;
|
|
resize: none;
|
|
overflow: hidden;
|
|
outline: none;
|
|
line-height: 1rem;
|
|
}
|
|
|
|
textarea::-webkit-scrollbar-corner {
|
|
background-color: transparent;
|
|
}
|
|
|
|
textarea::-webkit-resizer {
|
|
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAMAAAAolt3jAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAAD9QTFRFAAAAZWZmZmZmZmVmZWVmwsLBwsLCZ2ZmwsPCZmdlZWZnwcLBZmZkYGJjw8LDwsPBZmZnZWZkZ2ZkwMDBWFtcNbXb2AAAABV0Uk5TAP///////////////////////1H/YDRrSAAAAFBJREFUeJxVjUESgCAMA2mqAoqK6P/f6kzjIXIos5NumpI8g5LbpJnNQvDl52mWUYTquqnXwstshpHaTi+o+hHXccoKmHVW9yvIxv218ntivmOYAWpLfqaRAAAAAElFTkSuQmCC);
|
|
background-size: 7px;
|
|
background-repeat: no-repeat;
|
|
background-position: bottom 1px right 1px;
|
|
}
|
|
|
|
textarea::placeholder {
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
textarea::-webkit-scrollbar {
|
|
width: 7px;
|
|
height: 7px;
|
|
}
|
|
|
|
textarea::-webkit-scrollbar-track {
|
|
background-color: transparent;
|
|
}
|
|
|
|
textarea::-webkit-scrollbar-thumb {
|
|
background-color: var(--color-surface-subtle);
|
|
border-radius: var(--border-radius-md);
|
|
}
|
|
|
|
.dragover {
|
|
position: absolute;
|
|
opacity: 0.5;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: var(--color-surface-canvas);
|
|
}
|
|
</style>
|
|
|
|
<div
|
|
style:border={`1px solid ${focussed ? "var(--color-border-brand)" : borderColors[borderVariant]}`}
|
|
style:border-radius="var(--border-radius-sm)"
|
|
style:display="flex"
|
|
style:gap="0.5rem"
|
|
style:align-items={styleAlignItems}
|
|
style:background-color="var(--color-surface-base)"
|
|
style:position="relative"
|
|
style:width="100%"
|
|
style:min-height={styleMinHeight}>
|
|
<textarea
|
|
style:min-height={styleMinHeight}
|
|
style:padding={stylePadding}
|
|
tabindex="0"
|
|
bind:this={textareaElement}
|
|
bind:value
|
|
aria-label="textarea-comment"
|
|
class="txt-body-m-regular"
|
|
style:resize={size === "resizable" ? "vertical" : undefined}
|
|
style:overflow={size === "resizable" || size === "fixed-height"
|
|
? "scroll"
|
|
: undefined}
|
|
{placeholder}
|
|
{onpaste}
|
|
{oninput}
|
|
{onkeypress}
|
|
onfocus={() => (focussed = true)}
|
|
onblur={() => (focussed = false)}
|
|
onkeydown={handleKeydown}>
|
|
</textarea>
|
|
{#if draggingOver}
|
|
<div class="txt-body-m-regular dragover">
|
|
Drop files to add them as embeds. Embeds are limited to 10MB.
|
|
</div>
|
|
{/if}
|
|
</div>
|