mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-16 03:40:37 +08:00
Drop files only into the textarea under the cursor
This commit is contained in:
@@ -1,13 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { MentionInsertion } from "@app/components/MentionAutocomplete.svelte";
|
||||
import type { Embed } from "@bindings/cob/thread/Embed";
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import type { ComponentProps, Snippet } from "svelte";
|
||||
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import debounce from "lodash/debounce";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { SvelteMap } from "svelte/reactivity";
|
||||
|
||||
import { basename } from "@app/lib/embeds";
|
||||
@@ -123,9 +120,10 @@
|
||||
$state(undefined);
|
||||
let draggingOver = $state(false);
|
||||
let embedUploadError: string | undefined = $state();
|
||||
let dragEnterUnlistenFn: UnlistenFn | undefined = undefined;
|
||||
let dragLeaveUnlistenFn: UnlistenFn | undefined = undefined;
|
||||
let dragDropUnlistenFn: UnlistenFn | undefined = undefined;
|
||||
let dragDepth = 0;
|
||||
|
||||
// Mirrors `MAX_EMBED_SIZE` in `crates/radicle-types/src/traits/thread.rs`.
|
||||
const MAX_EMBED_SIZE = 10_485_760;
|
||||
|
||||
const restoreDragDropText = debounce(() => {
|
||||
embedUploadError = undefined;
|
||||
@@ -178,33 +176,60 @@
|
||||
textareaElement?.setSelectionRange(selectionStart, selectionEnd);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
if (attachEnabled) {
|
||||
dragEnterUnlistenFn = await listen("tauri://drag-enter", () => {
|
||||
draggingOver = true;
|
||||
});
|
||||
|
||||
dragLeaveUnlistenFn = await listen("tauri://drag-leave", () => {
|
||||
draggingOver = false;
|
||||
});
|
||||
|
||||
dragDropUnlistenFn = await listen<{
|
||||
paths: string[];
|
||||
position: { x: number; y: number };
|
||||
}>("tauri://drag-drop", async event => {
|
||||
draggingOver = false;
|
||||
await uploadEmbeds(event.payload.paths, basename, saveByPath);
|
||||
});
|
||||
}
|
||||
async function saveFileBytes(file: File) {
|
||||
if (file.size > MAX_EMBED_SIZE) {
|
||||
throw new Error(`${file.name} exceeds the embed size limit`);
|
||||
}
|
||||
});
|
||||
return invoke<string>("save_embed_by_bytes", {
|
||||
rid,
|
||||
name: file.name,
|
||||
bytes: new Uint8Array(await file.arrayBuffer()),
|
||||
});
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (dragEnterUnlistenFn) dragEnterUnlistenFn();
|
||||
if (dragLeaveUnlistenFn) dragLeaveUnlistenFn();
|
||||
if (dragDropUnlistenFn) dragDropUnlistenFn();
|
||||
});
|
||||
function hasFiles(event: DragEvent): boolean {
|
||||
return event.dataTransfer?.types.includes("Files") ?? false;
|
||||
}
|
||||
|
||||
function acceptsDrop(event: DragEvent): boolean {
|
||||
return attachEnabled && !preview && hasFiles(event);
|
||||
}
|
||||
|
||||
function handleDragEnter(event: DragEvent) {
|
||||
if (!hasFiles(event)) return;
|
||||
dragDepth += 1;
|
||||
if (acceptsDrop(event)) {
|
||||
event.preventDefault();
|
||||
draggingOver = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragOver(event: DragEvent) {
|
||||
if (!acceptsDrop(event) || !event.dataTransfer) return;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
|
||||
function handleDragLeave(event: DragEvent) {
|
||||
if (!hasFiles(event)) return;
|
||||
dragDepth = Math.max(0, dragDepth - 1);
|
||||
if (dragDepth === 0) {
|
||||
draggingOver = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop(event: DragEvent) {
|
||||
if (!hasFiles(event)) return;
|
||||
dragDepth = 0;
|
||||
draggingOver = false;
|
||||
if (!acceptsDrop(event) || !event.dataTransfer) return;
|
||||
event.preventDefault();
|
||||
void uploadEmbeds(
|
||||
Array.from(event.dataTransfer.files),
|
||||
file => file.name,
|
||||
saveFileBytes,
|
||||
);
|
||||
}
|
||||
|
||||
async function attachEmbedsByPaths(paths: string[]) {
|
||||
await uploadEmbeds(paths, basename, saveByPath);
|
||||
@@ -228,16 +253,7 @@
|
||||
invoke<string>("save_embed_by_clipboard", { name: file.name, rid }),
|
||||
);
|
||||
} else {
|
||||
await uploadEmbeds(
|
||||
files,
|
||||
file => file.name,
|
||||
async file =>
|
||||
invoke<string>("save_embed_by_bytes", {
|
||||
rid,
|
||||
name: file.name,
|
||||
bytes: new Uint8Array(await file.arrayBuffer()),
|
||||
}),
|
||||
);
|
||||
await uploadEmbeds(files, file => file.name, saveFileBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,6 +426,10 @@
|
||||
<div
|
||||
class="comment-section"
|
||||
aria-label="extended-textarea"
|
||||
ondragenter={handleDragEnter}
|
||||
ondragover={handleDragOver}
|
||||
ondragleave={handleDragLeave}
|
||||
ondrop={handleDrop}
|
||||
class:inline
|
||||
onkeydown={event => {
|
||||
if (!preview) return;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export function disableDropNavigation(): void {
|
||||
const isNativeTextDrop = (event: DragEvent) =>
|
||||
!(event.dataTransfer?.types.includes("Files") ?? false) &&
|
||||
event.target instanceof Element &&
|
||||
event.target.closest(
|
||||
'input, textarea, [contenteditable]:not([contenteditable="false"])',
|
||||
) !== null;
|
||||
|
||||
window.addEventListener("dragover", event => {
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
isNativeTextDrop(event) ||
|
||||
!event.dataTransfer
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "none";
|
||||
});
|
||||
window.addEventListener("drop", event => {
|
||||
if (!isNativeTextDrop(event)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2,10 +2,12 @@ import "overlayscrollbars/overlayscrollbars.css";
|
||||
import { mount } from "svelte";
|
||||
|
||||
import App from "./App.svelte";
|
||||
import { disableDropNavigation } from "./lib/disableDropNavigation";
|
||||
import { disableNativeContextMenu } from "./lib/disableNativeContextMenu";
|
||||
|
||||
const app = mount(App, { target: document.body });
|
||||
|
||||
disableNativeContextMenu();
|
||||
disableDropNavigation();
|
||||
|
||||
export default app;
|
||||
|
||||
Reference in New Issue
Block a user