-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Fix image drop from main editor #8115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
aae1974
2f5757f
b238659
3675d3b
68e9e6e
f9079ea
4bdca1a
2a04f4e
4a86b55
6f6303c
e2bb202
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -57,10 +57,13 @@ function TipTapEditorInner(props: TipTapEditorProps) { | |||||
const historyLength = useAppSelector((store) => store.session.history.length); | ||||||
const isInEdit = useAppSelector((store) => store.session.isInEdit); | ||||||
|
||||||
const [showDragOverMsg, setShowDragOverMsg] = useState(false); | ||||||
|
||||||
const { editor, onEnterRef } = createEditorConfig({ | ||||||
props, | ||||||
ideMessenger, | ||||||
dispatch, | ||||||
setShowDragOverMsg, | ||||||
}); | ||||||
|
||||||
// Register the main editor with the provider | ||||||
|
@@ -137,8 +140,6 @@ function TipTapEditorInner(props: TipTapEditorProps) { | |||||
} | ||||||
}, [isStreaming, props.isMainInput]); | ||||||
|
||||||
const [showDragOverMsg, setShowDragOverMsg] = useState(false); | ||||||
|
||||||
const [activeKey, setActiveKey] = useState<string | null>(null); | ||||||
|
||||||
const insertCharacterWithWhitespace = useCallback( | ||||||
|
@@ -221,40 +222,23 @@ function TipTapEditorInner(props: TipTapEditorProps) { | |||||
if (e.shiftKey) { | ||||||
setShowDragOverMsg(false); | ||||||
} else { | ||||||
setTimeout(() => setShowDragOverMsg(false), 2000); | ||||||
setTimeout(() => { | ||||||
setShowDragOverMsg(false); | ||||||
}, 2000); | ||||||
} | ||||||
} | ||||||
setShowDragOverMsg(false); | ||||||
}} | ||||||
onDragEnter={() => { | ||||||
setShowDragOverMsg(true); | ||||||
}} | ||||||
onDragEnd={() => { | ||||||
setShowDragOverMsg(false); | ||||||
}} | ||||||
onDrop={(event) => { | ||||||
// Just hide the drag overlay - ProseMirror handles the actual drop | ||||||
setShowDragOverMsg(false); | ||||||
if ( | ||||||
!defaultModel || | ||||||
!modelSupportsImages( | ||||||
defaultModel.provider, | ||||||
defaultModel.model, | ||||||
defaultModel.title, | ||||||
defaultModel.capabilities, | ||||||
) | ||||||
) { | ||||||
return; | ||||||
} | ||||||
let file = event.dataTransfer.files[0]; | ||||||
void handleImageFile(ideMessenger, file).then((result) => { | ||||||
if (!editor) { | ||||||
return; | ||||||
} | ||||||
if (result) { | ||||||
const [_, dataUrl] = result; | ||||||
const { schema } = editor.state; | ||||||
const node = schema.nodes.image.create({ src: dataUrl }); | ||||||
const tr = editor.state.tr.insert(0, node); | ||||||
editor.view.dispatch(tr); | ||||||
} | ||||||
}); | ||||||
event.preventDefault(); | ||||||
// Let the event bubble to ProseMirror by not preventing default | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dropping a file on InputBoxDiv areas outside the ProseMirror view (like the toolbar) now navigates the browser to that file because this handler no longer calls event.preventDefault(), and the new drop plugin in utils/editorConfig.ts only runs for drops that originate on the editor DOM. Please continue to cancel the default action here to keep the page from navigating away. Prompt for AI agents
Suggested change
|
||||||
}} | ||||||
> | ||||||
<div className="px-2.5 pb-1 pt-2"> | ||||||
|
@@ -299,9 +283,7 @@ function TipTapEditorInner(props: TipTapEditorProps) { | |||||
defaultModel?.model || "", | ||||||
defaultModel?.title, | ||||||
defaultModel?.capabilities, | ||||||
) && ( | ||||||
<DragOverlay show={showDragOverMsg} setShow={setShowDragOverMsg} /> | ||||||
)} | ||||||
) && <DragOverlay show={showDragOverMsg} />} | ||||||
<div id={TIPPY_DIV_ID} className="fixed z-50" /> | ||||||
</InputBoxDiv> | ||||||
); | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ import { | |
getContextProviderDropdownOptions, | ||
getSlashCommandDropdownOptions, | ||
} from "./getSuggestion"; | ||
import { handleImageFile } from "./imageUtils"; | ||
import { handleImageFile, handleVSCodeResourceFromHtml } from "./imageUtils"; | ||
|
||
export function getPlaceholderText( | ||
placeholder: TipTapEditorProps["placeholder"], | ||
|
@@ -69,8 +69,9 @@ export function createEditorConfig(options: { | |
props: TipTapEditorProps; | ||
ideMessenger: IIdeMessenger; | ||
dispatch: AppDispatch; | ||
setShowDragOverMsg: (show: boolean) => void; | ||
}) { | ||
const { props, ideMessenger, dispatch } = options; | ||
const { props, ideMessenger, dispatch, setShowDragOverMsg } = options; | ||
|
||
const posthog = usePostHog(); | ||
|
||
|
@@ -147,6 +148,80 @@ export function createEditorConfig(options: { | |
const plugin = new Plugin({ | ||
props: { | ||
handleDOMEvents: { | ||
drop(view, event) { | ||
// Hide drag overlay immediately when drop is handled | ||
setShowDragOverMsg(false); | ||
|
||
// Get current model and check if it supports images | ||
const model = defaultModelRef.current; | ||
if ( | ||
!model || | ||
!modelSupportsImages( | ||
model.provider, | ||
model.model, | ||
model.title, | ||
model.capabilities, | ||
) | ||
) { | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
return true; | ||
} | ||
|
||
event.preventDefault(); | ||
event.stopPropagation(); | ||
|
||
// Check if dataTransfer exists | ||
if (!event.dataTransfer) { | ||
return true; | ||
} | ||
|
||
// Handle file drop first | ||
if (event.dataTransfer.files.length > 0) { | ||
const file = event.dataTransfer.files[0]; | ||
void handleImageFile(ideMessenger, file).then((result) => { | ||
if (result) { | ||
const [_, dataUrl] = result; | ||
const { schema } = view.state; | ||
const node = schema.nodes.image.create({ | ||
src: dataUrl, | ||
}); | ||
const tr = view.state.tr.insert(0, node); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The drop handler always inserts the image at document position 0, so every dropped image appears at the top instead of the drop point. Please compute the drop insertion position (e.g., using the drop selection or view.posAtCoords) before inserting. Prompt for AI agents
|
||
view.dispatch(tr); | ||
} | ||
}); | ||
return true; | ||
} | ||
|
||
// Handle drop of HTML content (including VS Code resource URLs) | ||
const html = event.dataTransfer.getData("text/html"); | ||
if (html) { | ||
void handleVSCodeResourceFromHtml(ideMessenger, html) | ||
.then((dataUrl) => { | ||
if (dataUrl) { | ||
const { schema } = view.state; | ||
const node = schema.nodes.image.create({ | ||
src: dataUrl, | ||
}); | ||
const tr = view.state.tr.insert(0, node); | ||
view.dispatch(tr); | ||
} | ||
}) | ||
.catch((err) => | ||
console.error( | ||
"Failed to handle VS Code resource:", | ||
err, | ||
), | ||
); | ||
} | ||
|
||
return true; | ||
}, | ||
dragover(view, event) { | ||
// Allow dragover for proper drop handling | ||
event.preventDefault(); | ||
return true; | ||
}, | ||
paste(view, event) { | ||
const model = defaultModelRef.current; | ||
if (!model) return; | ||
|
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uppercase image extensions (e.g. foo.PNG) hit this branch and get labeled image/jpeg even though the bytes are PNG, producing a mismatched data URL that can fail to render; normalize the extension before comparing.
Prompt for AI agents