fix: Pasted content sometimes appears in plaintext (#12638)

* fix: 'Stuck' shift key forces plaintext paste
fix: Link on image does not survive copy/paste

* sanitize
This commit is contained in:
Tom Moor
2026-06-09 07:38:36 -04:00
committed by GitHub
parent 39623b90bd
commit 95106e695f
2 changed files with 34 additions and 24 deletions
+2 -6
View File
@@ -55,15 +55,11 @@ export default class PasteHandler extends Extension {
},
handleDOMEvents: {
keydown: (_, event) => {
if (event.key === "Shift") {
this.shiftKey = true;
}
this.shiftKey = event.shiftKey;
return false;
},
keyup: (_, event) => {
if (event.key === "Shift") {
this.shiftKey = false;
}
this.shiftKey = event.shiftKey;
return false;
},
},
+25 -11
View File
@@ -9,12 +9,12 @@ import type {
import type { Command } from "prosemirror-state";
import { NodeSelection, Plugin, TextSelection } from "prosemirror-state";
import * as React from "react";
import { sanitizeImageSrc } from "../../utils/urls";
import { sanitizeImageSrc, sanitizeUrl } from "../../utils/urls";
import Caption from "../components/Caption";
import ImageComponent from "../components/Image";
import type { MarkdownSerializerState } from "../lib/markdown/serializer";
import { EditorStyleHelper } from "../styles/EditorStyleHelper";
import type { ComponentProps } from "../types";
import type { ComponentProps, NodeAttrMark } from "../types";
import SimpleImage from "./SimpleImage";
import { LightboxImageFactory } from "../lib/Lightbox";
import { ImageSource } from "../lib/FileHelper";
@@ -151,6 +151,12 @@ export default class Image extends SimpleImage {
const width = img?.getAttribute("width");
const height = img?.getAttribute("height");
// A link wrapping the image is stored as a node attribute rather
// than a mark, parse it back so it survives copy/paste. Sanitize
// the href as it is rendered directly into the DOM by the view.
const href = sanitizeUrl(img?.closest("a")?.getAttribute("href"));
return {
src: img?.getAttribute("src"),
alt: img?.getAttribute("alt"),
@@ -159,17 +165,16 @@ export default class Image extends SimpleImage {
width: width ? parseInt(width, 10) : undefined,
height: height ? parseInt(height, 10) : undefined,
layoutClass,
marks: href ? [{ type: "link", attrs: { href } }] : undefined,
};
},
},
{
tag: "img",
getAttrs: (dom: HTMLImageElement) => {
// Don't parse images from our own editor with this rule.
if (
dom.parentElement?.classList.contains("image") ||
dom.parentElement?.classList.contains("emoji")
) {
// Don't parse images from our own editor with this rule. A linked
// image nests the <img> inside an <a>, so check ancestors too.
if (dom.closest(".image") || dom.closest(".emoji")) {
return false;
}
@@ -206,19 +211,28 @@ export default class Image extends SimpleImage {
? `image image-${node.attrs.layoutClass}`
: "image";
const children = [
[
// `marks` is held separately below and is not a valid DOM attribute.
const { marks, ...attrs } = node.attrs;
const img = [
"img",
{
...node.attrs,
...attrs,
src: sanitizeImageSrc(node.attrs.src),
width: node.attrs.width,
height: node.attrs.height,
contentEditable: "false",
},
],
];
// A link applied to an image is held as a node attribute rather than a
// mark, so it must be written into the DOM explicitly here.
const linkHref = (marks as NodeAttrMark[] | undefined)?.find(
(mark) => mark.type === "link"
)?.attrs?.href;
const href = typeof linkHref === "string" ? linkHref : undefined;
const children = [href ? ["a", { href: sanitizeUrl(href) }, img] : img];
if (node.attrs.alt) {
children.push([
"p",