mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
feat: drag relevant notes and sources into editor to insert wikilinks (#2288)
* feat: drag relevant notes and sources into editor to insert wikilinks Adds Obsidian-native drag-and-drop support to the Relevant Notes panel and Sources modal, using app.dragManager so dropping onto an editor automatically inserts [[wikilink]] without any custom drop handler. - New useNoteDrag hook wraps app.dragManager.dragLink/onDragStart - RelevantNotes: title link (expanded) and badge (collapsed) are draggable - SourcesModal: source links are draggable (vanilla JS class context) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: prevent drop zone overlay when dragging from relevant notes The chat container's "drop files here" overlay incorrectly appeared when dragging notes from the relevant notes pane. Mark internal drags with a custom data transfer type so the drop zone hook can skip them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ec5cdcfe25
commit
cd726a0e83
4 changed files with 70 additions and 2 deletions
|
|
@ -6,6 +6,7 @@ import { HelpTooltip } from "@/components/ui/help-tooltip";
|
|||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useChatInput } from "@/context/ChatInputContext";
|
||||
import { useActiveFile } from "@/hooks/useActiveFile";
|
||||
import { useNoteDrag } from "@/hooks/useNoteDrag";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { logWarn } from "@/logger";
|
||||
import { SemanticSearchToggleModal } from "@/components/modals/SemanticSearchToggleModal";
|
||||
|
|
@ -131,6 +132,7 @@ function RelevantNote({
|
|||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [fileContent, setFileContent] = useState<string | null>(null);
|
||||
const handleDragStart = useNoteDrag();
|
||||
|
||||
const loadContent = useCallback(async () => {
|
||||
if (fileContent) return; // Don't load if we already have content
|
||||
|
|
@ -181,6 +183,13 @@ function RelevantNote({
|
|||
|
||||
<div className="tw-flex-1 tw-overflow-hidden">
|
||||
<a
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
const file = app.vault.getAbstractFileByPath(note.document.path);
|
||||
if (file instanceof TFile) {
|
||||
handleDragStart(e, file);
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const openInNewLeaf = e.metaKey || e.ctrlKey;
|
||||
|
|
@ -194,7 +203,7 @@ function RelevantNote({
|
|||
}
|
||||
}}
|
||||
className="tw-block tw-w-full tw-truncate tw-text-sm tw-font-bold tw-text-normal"
|
||||
title={note.document.title}
|
||||
title={`${note.document.title} - drag to insert wikilink`}
|
||||
>
|
||||
{note.document.title}
|
||||
</a>
|
||||
|
|
@ -293,6 +302,7 @@ export const RelevantNotes = memo(
|
|||
const activeFile = useActiveFile();
|
||||
const chatInput = useChatInput();
|
||||
const hasIndex = useHasIndex(activeFile?.path ?? "", refresher);
|
||||
const handleDragStart = useNoteDrag();
|
||||
const navigateToNote = (notePath: string, openInNewLeaf = false) => {
|
||||
const file = app.vault.getAbstractFileByPath(notePath);
|
||||
if (file instanceof TFile) {
|
||||
|
|
@ -404,7 +414,15 @@ export const RelevantNotes = memo(
|
|||
<Badge
|
||||
variant="outline"
|
||||
key={note.document.path}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
const file = app.vault.getAbstractFileByPath(note.document.path);
|
||||
if (file instanceof TFile) {
|
||||
handleDragStart(e, file);
|
||||
}
|
||||
}}
|
||||
className="tw-max-w-40 tw-text-xs tw-text-muted hover:tw-cursor-pointer hover:tw-bg-interactive-hover"
|
||||
title={`${note.document.title} - drag to insert wikilink`}
|
||||
>
|
||||
<span className="tw-truncate">{note.document.title}</span>
|
||||
</Badge>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// src/components/SourcesModal.tsx
|
||||
import { App, Modal } from "obsidian";
|
||||
import { App, Modal, TFile } from "obsidian";
|
||||
|
||||
export class SourcesModal extends Modal {
|
||||
sources: { title: string; path: string; score: number; explanation?: any }[];
|
||||
|
|
@ -55,6 +55,19 @@ export class SourcesModal extends Modal {
|
|||
href: `obsidian://open?vault=${encodeURIComponent(this.app.vault.getName())}&file=${encodeURIComponent(source.path || source.title)}`,
|
||||
text: displayText,
|
||||
});
|
||||
link.title = `${displayText} - drag to insert wikilink`;
|
||||
link.draggable = true;
|
||||
link.addEventListener("dragstart", (e) => {
|
||||
const filePath = source.path || source.title;
|
||||
const file = this.app.vault.getAbstractFileByPath(filePath);
|
||||
if (file instanceof TFile) {
|
||||
const dragManager = (this.app as any).dragManager;
|
||||
if (!dragManager) return;
|
||||
const linkText = this.app.metadataCache.fileToLinktext(file, "");
|
||||
const dragData = dragManager.dragLink(e, linkText);
|
||||
dragManager.onDragStart(e, dragData);
|
||||
}
|
||||
});
|
||||
link.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -104,6 +104,11 @@ export function useChatFileDrop(props: UseChatFileDropProps): UseChatFileDropRet
|
|||
if (e.dataTransfer) {
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
|
||||
// Skip showing drop zone for drags originating from within the plugin (e.g. relevant notes)
|
||||
if (e.dataTransfer.types.includes("copilot/internal-drag")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have string items (Obsidian nav bar drag) or file items (external files)
|
||||
const hasStringItems = Array.from(e.dataTransfer.items).some(
|
||||
(item) => item.kind === "string"
|
||||
|
|
|
|||
32
src/hooks/useNoteDrag.ts
Normal file
32
src/hooks/useNoteDrag.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { useCallback } from "react";
|
||||
import { TFile } from "obsidian";
|
||||
|
||||
/**
|
||||
* Returns a drag-start handler that integrates with Obsidian's native dragManager API.
|
||||
* When a note element is dropped onto the Obsidian editor, the editor automatically
|
||||
* inserts the corresponding `[[wikilink]]` without any additional drop handler.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* const handleDragStart = useNoteDrag();
|
||||
* const file = app.vault.getAbstractFileByPath(path);
|
||||
* if (file instanceof TFile) {
|
||||
* <div draggable onDragStart={(e) => handleDragStart(e, file)}> ... </div>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useNoteDrag() {
|
||||
const handleDragStart = useCallback((e: React.DragEvent, file: TFile): void => {
|
||||
const dragManager = (app as any).dragManager;
|
||||
if (!dragManager) return;
|
||||
|
||||
// Mark this drag as internal so the chat drop zone overlay doesn't appear
|
||||
e.dataTransfer.setData("copilot/internal-drag", "true");
|
||||
|
||||
const linkText = app.metadataCache.fileToLinktext(file, "");
|
||||
const dragData = dragManager.dragLink(e.nativeEvent, linkText);
|
||||
dragManager.onDragStart(e.nativeEvent, dragData);
|
||||
}, []);
|
||||
|
||||
return handleDragStart;
|
||||
}
|
||||
Loading…
Reference in a new issue