diff --git a/packages/core/src/editor/editor.css b/packages/core/src/editor/editor.css index 6aeaea0d33..be9ed67002 100644 --- a/packages/core/src/editor/editor.css +++ b/packages/core/src/editor/editor.css @@ -66,28 +66,6 @@ pointer-events: none; } -.bn-drag-preview { - position: absolute; - top: 0; - left: 0; - padding: 10px; - /* Sort of a hack but seems like the most reliable solution. */ - /* Drag preview element needs to be within bounds of the document area or it - won't work in some cases. */ - /* Negative z-index covers most cases, but the element can still be visible - if UI elements are translucent. */ - /* Setting opacity has no effect on the drag preview but does affect the - element. Unless it's set to 0, in which case the drag preview also becomes - hidden. So setting it to an extremely low value instead makes the element - functionally invisible while not affecting the drag preview itself. */ - opacity: 0.001; - /* The element is kept in the DOM after setDragImage captures its snapshot, - so it can overlap the editor and block drops in that area. Disabling - pointer-events lets drag/drop events pass through to the editor while - leaving the captured preview unaffected. */ - pointer-events: none; -} - .bn-editor .bn-collaboration-cursor__base { position: relative; } diff --git a/packages/core/src/extensions/SideMenu/dragging.ts b/packages/core/src/extensions/SideMenu/dragging.ts index f90f2cf55a..a6e8379a0e 100644 --- a/packages/core/src/extensions/SideMenu/dragging.ts +++ b/packages/core/src/extensions/SideMenu/dragging.ts @@ -61,49 +61,76 @@ function setDragImage(view: EditorView, from: number, to = from) { to += view.state.doc.resolve(from + 1).node().nodeSize; } - // The preview is a copy of the element that holds the dragged blocks, - // without its other children: a copy of each dragged block's element. The - // holding element can have no box of its own (`display: contents`, e.g. the - // slot of a React layout block), but the preview needs one. - const preview = view.domAtPos(from).node.cloneNode(false) as HTMLElement; - preview.style.removeProperty("display"); + // A copy of the element that holds the dragged blocks, without its other + // children: a copy of each dragged block's element. The holding element can + // have no box of its own (`display: contents`, e.g. the slot of a React + // layout block), but the preview needs one. + const blocks = view.domAtPos(from).node.cloneNode(false) as HTMLElement; + blocks.style.removeProperty("display"); for (let pos = from; pos < to; pos += view.state.doc.nodeAt(pos)!.nodeSize) { const blockDOM = view.nodeDOM(pos); if (!blockDOM) { throw new Error(`The dragged block at ${pos} has no DOM.`); } - preview.appendChild(blockDOM.cloneNode(true)); + blocks.appendChild(blockDOM.cloneNode(true)); } - // dataTransfer.setDragImage(element) only works if element is attached to the DOM. - unsetDragImage(view.root); - dragImageElement = preview; - // Browsers may have CORS policies which prevents iframes from being // manipulated, so better to stay on the safe side and remove them from the // drag preview. The drag preview doesn't work with embedded documents // (iframe/embed/object) anyway, and including an (e.g. a PDF) // can prevent the drag from initiating at all. - const embeddedDocs = dragImageElement.querySelectorAll( - "iframe, embed, object", - ); - embeddedDocs.forEach((el) => el.parentElement?.removeChild(el)); - - // TODO: This is hacky, need a better way of assigning classes to the editor so that they can also be applied to the - // drag preview. - const classes = view.dom.className.split(" "); - const inheritedClasses = classes - .filter( - (className) => - className !== "ProseMirror" && - className !== "bn-root" && - className !== "bn-editor", - ) - .join(" "); - - dragImageElement.className = - dragImageElement.className + " bn-drag-preview " + inheritedClasses; + blocks + .querySelectorAll("iframe, embed, object") + .forEach((el) => el.parentElement?.removeChild(el)); + + // The preview is outside the editor, so it copies what styles the blocks + // there: the theme of the editor's container (classes, color scheme, theme + // variables), the editor's classes, and the blocks' width, so that text + // wraps and images scale as in the editor. + const container = view.dom.parentElement?.closest(".bn-root"); + const preview = + (container?.cloneNode(false) as HTMLElement | undefined) ?? + document.createElement("div"); + preview.removeAttribute("id"); + preview.classList.add("bn-drag-preview"); + const editor = document.createElement("div"); + editor.className = view.dom.className; + editor.appendChild(blocks); + preview.appendChild(editor); + + // The layout is inline, so that app CSS on the copied classes (e.g. a height + // on `.bn-editor`) does not change it. The preview is off-screen, as the + // browser only needs it for its snapshot: `opacity` would also hide it in + // the snapshot in Firefox and Safari. Taller than the cap, it would reach + // into the page from above. + Object.assign(preview.style, { + background: "transparent", + position: "absolute", + top: "-10000px", + left: "0", + margin: "0", + padding: "10px", + height: "auto", + minHeight: "0", + maxHeight: "75vh", + overflow: "hidden", + }); + Object.assign(editor.style, { + // Keep the editor's background out of the empty space around the blocks. + background: "transparent", + width: `${(view.nodeDOM(from) as HTMLElement).offsetWidth}px`, + height: "auto", + minHeight: "0", + maxHeight: "none", + margin: "0", + padding: "0", + overflow: "visible", + }); + // dataTransfer.setDragImage(element) only works if element is attached to the DOM. + unsetDragImage(view.root); + dragImageElement = preview; if (view.root instanceof ShadowRoot) { view.root.appendChild(dragImageElement); } else { diff --git a/tests/src/end-to-end/dragdrop/dragdrop.test.tsx b/tests/src/end-to-end/dragdrop/dragdrop.test.tsx index 472df7ad4b..dc98e87b6b 100644 --- a/tests/src/end-to-end/dragdrop/dragdrop.test.tsx +++ b/tests/src/end-to-end/dragdrop/dragdrop.test.tsx @@ -391,6 +391,42 @@ describe.skipIf(browserName === "firefox")("Dragging layout blocks", () => { } }); + // The preview is outside the editor, so it copies the editor's theme, + // classes and width, and app CSS that sizes the editor does not apply. + test("draws the drag preview like the editor", async () => { + const appCSS = document.createElement("style"); + appCSS.textContent = ".bn-editor { min-height: 500px; }"; + document.head.appendChild(appCSS); + await renderEditors([ + { + id: "table", + type: "table", + content: { type: "tableContent", rows: [{ cells: ["A", "B"] }] }, + }, + ]); + + await startBlockDrag(handleOf("table")); + try { + const preview = await waitForSelector(".bn-drag-preview"); + const copy = preview.querySelector('[data-id="table"]')!; + expect(copy.getBoundingClientRect().width).toBe( + block("table").getBoundingClientRect().width, + ); + expect(preview.getBoundingClientRect().height).toBeLessThan(500); + expect( + getComputedStyle(preview.querySelector("td")!).borderTopStyle, + ).toBe("solid"); + expect(preview.getAttribute("data-color-scheme")).toBe( + document + .querySelector(".bn-container")! + .getAttribute("data-color-scheme"), + ); + } finally { + await dropDraggedBlock(block("table"), true); + appCSS.remove(); + } + }); + test("moves a namedOnly layout block in its list, without a copy of the list", async () => { await renderEditors([ { @@ -454,6 +490,8 @@ describe.skipIf(browserName === "firefox")("Dropping into a block", () => { steps: 5, }, ]); + // The drop goes into the toggle once it is highlighted. + await waitForSelector("[data-drop-target]"); // Hovers before the drop, as a user does, which gave ProseMirror the time // to draw the toggle again. await sleep(200);