Improve attachment interactions on textareas

Signed-off-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
Rūdolfs Ošiņš
2023-10-26 18:17:52 +02:00
parent c1740beb12
commit ab88f31eef
4 changed files with 119 additions and 43 deletions
+93 -37
View File
@@ -28,6 +28,9 @@
let newEmbeds: EmbedWithOid[] = [];
let selectionStart = 0;
let selectionEnd = 0;
let inputFiles: FileList | undefined = undefined;
const inputId = `input-label-${crypto.randomUUID()}`;
const dispatch = createEventDispatcher<{
submit: { comment: string; embeds: Embed[] };
@@ -43,48 +46,79 @@
const MAX_BLOB_SIZE = 4_194_304;
function handleFileDrop(event: DragEvent) {
function handleFileDrop(event: { detail: DragEvent }) {
if (!enableAttachments) {
return;
}
event.detail.preventDefault();
if (event.detail.dataTransfer) {
attachEmbeds(event.detail.dataTransfer.files);
}
}
function handleFilePaste(event: ClipboardEvent) {
// Always allow pasting text content.
if (event.clipboardData && event.clipboardData.files.length === 0) {
return;
}
if (!enableAttachments) {
return;
}
event.preventDefault();
if (event.dataTransfer) {
const embeds = Array.from(event.dataTransfer.files).map(embed);
void Promise.all(embeds).then(embeds =>
embeds.forEach(embed => {
if (embed.content.length > MAX_BLOB_SIZE) {
modal.show({
component: ErrorModal,
props: {
title: "File too large",
subtitle: [
"The file you tried to upload is too large.",
"The maximum file size is 4MB.",
],
error: { message: `File ${embed.name} is too large` },
},
});
return;
}
newEmbeds = [
...newEmbeds,
{
oid: embed.oid,
name: embed.name,
content: embed.content,
},
];
const embedText = `![${embed.name}](${embed.oid})\n`;
body = body
.slice(0, selectionStart)
.concat(embedText, body.slice(selectionEnd));
selectionStart += embedText.length;
selectionEnd = selectionStart;
}),
);
if (event.clipboardData) {
attachEmbeds(event.clipboardData.files);
}
}
function handleFileSelect(event: Event) {
if (!enableAttachments) {
return;
}
event.preventDefault();
if (inputFiles) {
attachEmbeds(inputFiles);
}
}
function attachEmbeds(files: FileList) {
const embeds = Array.from(files).map(embed);
void Promise.all(embeds).then(embeds =>
embeds.forEach(embed => {
if (embed.content.length > MAX_BLOB_SIZE) {
modal.show({
component: ErrorModal,
props: {
title: "File too large",
subtitle: [
"The file you tried to upload is too large.",
"The maximum file size is 4MB.",
],
error: { message: `File ${embed.name} is too large` },
},
});
return;
}
newEmbeds = [
...newEmbeds,
{
oid: embed.oid,
name: embed.name,
content: embed.content,
},
];
const embedText = `![${embed.name}](${embed.oid})\n`;
body = body
.slice(0, selectionStart)
.concat(embedText, body.slice(selectionEnd));
selectionStart += embedText.length;
selectionEnd = selectionStart;
}),
);
}
</script>
<style>
@@ -107,6 +141,7 @@
flex-direction: row;
align-items: center;
width: 100%;
gap: 1rem;
}
.buttons {
display: flex;
@@ -124,6 +159,12 @@
margin-left: 1px;
margin-top: 1px;
}
label {
color: var(--color-foreground-contrast);
}
label:hover {
color: var(--color-foreground-primary);
}
</style>
<div class="comment-section" class:inline>
@@ -153,8 +194,16 @@
<Markdown content={body} embeds={newEmbeds} />
</div>
{:else}
<input
multiple
bind:files={inputFiles}
style:display="none"
type="file"
id={inputId}
on:change={handleFileSelect} />
<Textarea
on:drop={handleFileDrop}
on:paste={handleFilePaste}
bind:selectionEnd
bind:selectionStart
{focus}
@@ -165,8 +214,15 @@
<div class="actions">
{#if !preview}
<div class="caption">
Markdown supported. {#if enableAttachments}Drop attachments into the
text area.{/if} Press {utils.isMac() ? "⌘" : "ctrl"}↵ to submit.
{#if enableAttachments}
Add files by dragging & dropping, <label
for={inputId}
style:cursor="pointer">
selecting
</label>
or pasting them.
{/if}
Markdown supported. Press {utils.isMac() ? "⌘" : "ctrl"}↵ to submit.
</div>
{/if}
<div class="buttons">
+6 -1
View File
@@ -1,9 +1,10 @@
<script lang="ts">
import Loading from "./Loading.svelte";
export let title: string | undefined = undefined;
export let ariaLabel: string | undefined = undefined;
export let inline: boolean = false;
export let loading: boolean = false;
export let title: string | undefined = undefined;
</script>
<style>
@@ -20,6 +21,9 @@
gap: 0.25rem;
font-size: var(--font-size-small);
}
.inline {
display: inline-flex;
}
.button:hover {
color: var(--color-foreground-contrast);
background-color: var(--color-fill-ghost);
@@ -36,6 +40,7 @@
aria-label={ariaLabel}
{title}
class="button"
class:inline
on:click>
<slot />
</div>
+16 -1
View File
@@ -49,8 +49,11 @@
const dispatch = createEventDispatcher<{
submit: null;
drop: DragEvent;
}>();
let dragging: boolean = false;
function handleKeydown(event: KeyboardEvent) {
const auxiliarKey = isMac() ? event.metaKey : event.ctrlKey;
if (auxiliarKey && event.key === "Enter") {
@@ -60,6 +63,11 @@
textareaElement?.blur();
}
}
function handleDropAndForward(event: DragEvent) {
dragging = false;
dispatch("drop", event);
}
</script>
<style>
@@ -106,6 +114,9 @@
textarea:focus {
border: 1px solid var(--color-fill-secondary);
}
.drag {
border: 1px dashed var(--color-fill-secondary) !important;
}
</style>
<textarea
@@ -114,10 +125,14 @@
aria-label="textarea-comment"
class="txt-small"
class:resizable
class:drag={dragging}
{placeholder}
on:change
on:click
on:input
on:drop
on:drop={handleDropAndForward}
on:paste
on:dragenter={() => (dragging = true)}
on:dragleave={() => (dragging = false)}
on:keydown|stopPropagation={handleKeydown}
on:keypress />
+4 -4
View File
@@ -37,10 +37,10 @@
const api = new HttpdClient(baseUrl);
function handleFileDrop(event: DragEvent) {
event.preventDefault();
if (event.dataTransfer) {
const embeds = Array.from(event.dataTransfer.files).map(embed);
function handleFileDrop(event: { detail: DragEvent }) {
event.detail.preventDefault();
if (event.detail.dataTransfer) {
const embeds = Array.from(event.detail.dataTransfer.files).map(embed);
void Promise.all(embeds).then(embeds =>
embeds.forEach(({ oid, name, content }) => {
newEmbeds = [