mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
fix: Dragging images in editor (#12647)
* fix: Editor image dragging * feedback
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { EditorAwareHTML5Backend } from "~/components/EditorAwareHTML5Backend";
|
||||
import ErrorSuspended from "~/scenes/Errors/ErrorSuspended";
|
||||
import Layout from "~/components/Layout";
|
||||
import RegisterKeyDown from "~/components/RegisterKeyDown";
|
||||
@@ -106,7 +106,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
|
||||
<DocumentContextProvider>
|
||||
<RightSidebarProvider>
|
||||
<PortalContext.Provider value={layoutRef.current}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<DndProvider backend={EditorAwareHTML5Backend}>
|
||||
<Layout title={team.name} sidebar={sidebar} ref={layoutRef}>
|
||||
<RegisterKeyDown trigger="n" handler={goToNewDocument} />
|
||||
<RegisterKeyDown trigger="t" handler={goToSearch} />
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { BackendFactory } from "dnd-core";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
|
||||
/**
|
||||
* react-dnd's HTML5 backend installs global capture-phase listeners on `window`
|
||||
* that call `preventDefault()` on drops whose dataTransfer resembles a native
|
||||
* item – including a dragged `<img>`, which is how ProseMirror serializes an
|
||||
* image drag.
|
||||
*
|
||||
* These handlers run before ProseMirror's, and they live on `window`, so a
|
||||
* propagation-based guard can't stop react-dnd without also starving the editor
|
||||
* of the event. Instead we wrap the backend and make its top-level capture
|
||||
* handlers no-op for events that occur within the editor surface.
|
||||
*/
|
||||
const captureHandlerNames = [
|
||||
"handleTopDragStartCapture",
|
||||
"handleTopDragEnterCapture",
|
||||
"handleTopDragOverCapture",
|
||||
"handleTopDragLeaveCapture",
|
||||
"handleTopDropCapture",
|
||||
"handleTopDragEndCapture",
|
||||
] as const;
|
||||
|
||||
const isWithinEditor = (target: EventTarget | null): boolean =>
|
||||
target instanceof Element && Boolean(target.closest(".ProseMirror"));
|
||||
|
||||
/**
|
||||
* An HTML5 drag-and-drop backend that ignores drag events originating within the
|
||||
* rich text editor so that ProseMirror can handle them itself.
|
||||
*
|
||||
* @param manager The drag-and-drop manager.
|
||||
* @param context The global context.
|
||||
* @param options Backend options.
|
||||
* @returns The wrapped HTML5 backend instance.
|
||||
*/
|
||||
export const EditorAwareHTML5Backend: BackendFactory = (
|
||||
manager,
|
||||
context,
|
||||
options
|
||||
) => {
|
||||
const backend = HTML5Backend(manager, context, options);
|
||||
|
||||
// The capture handlers are private instance fields on the backend, so reach
|
||||
// for them through an index signature view of the instance.
|
||||
const handlers = backend as unknown as Record<
|
||||
string,
|
||||
(event: DragEvent) => void
|
||||
>;
|
||||
|
||||
for (const name of captureHandlerNames) {
|
||||
const original = handlers[name];
|
||||
if (typeof original === "function") {
|
||||
handlers[name] = (event: DragEvent) => {
|
||||
if (isWithinEditor(event.target)) {
|
||||
return;
|
||||
}
|
||||
original.call(backend, event);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return backend;
|
||||
};
|
||||
@@ -46,6 +46,7 @@ function Caption({ placeholder, children, isSelected, width, ...rest }: Props) {
|
||||
tabIndex={-1}
|
||||
aria-label={t("Caption")}
|
||||
role="textbox"
|
||||
draggable={false}
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
data-caption={placeholder}
|
||||
|
||||
@@ -132,8 +132,7 @@ export default class Image extends SimpleImage {
|
||||
marks: "",
|
||||
group: "inline",
|
||||
selectable: true,
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1289000
|
||||
draggable: false,
|
||||
draggable: true,
|
||||
atom: true,
|
||||
parseDOM: [
|
||||
{
|
||||
@@ -260,6 +259,32 @@ export default class Image extends SimpleImage {
|
||||
commentedImagePlugin(),
|
||||
new Plugin({
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
dragstart: (_view, event) => {
|
||||
// ProseMirror lets the browser snapshot the dragged node's DOM as
|
||||
// the drag image. For images that DOM includes the caption area and
|
||||
// padding, which renders as a large white box around the image.
|
||||
// Substitute the image element so the drag ghost is tight to it.
|
||||
if (
|
||||
!(event.target instanceof HTMLElement) ||
|
||||
!event.dataTransfer
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const image = event.target
|
||||
.closest(`.component-${this.name}`)
|
||||
?.querySelector("img");
|
||||
if (image) {
|
||||
const rect = image.getBoundingClientRect();
|
||||
event.dataTransfer.setDragImage(
|
||||
image,
|
||||
event.clientX - rect.left,
|
||||
event.clientY - rect.top
|
||||
);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
handleKeyDown: (view, event) => {
|
||||
// prevent prosemirror's default spacebar behavior
|
||||
// & zoom in if the selected node is image
|
||||
|
||||
Reference in New Issue
Block a user