Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Improve attachment interactions on textareas
Rūdolfs Ošiņš committed 2 years ago
commit ab88f31eef49c211d145ed55f4dedf553be6dde4
parent c1740beb1260a67e21d506fb993ae61d70b2fdad
4 files changed +119 -43
modified src/components/ExtendedTextarea.svelte
@@ -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">
modified src/components/IconButton.svelte
@@ -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>
modified src/components/Textarea.svelte
@@ -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 />
modified src/views/projects/Issue/New.svelte
@@ -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 = [