mirror of
https://github.com/outline/outline.git
synced 2026-01-05 18:49:53 -06:00
fix: Emojis and embeds cannot be copied to plain text clipboard (#3561)
This commit is contained in:
43
shared/editor/lib/textBetween.ts
Normal file
43
shared/editor/lib/textBetween.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Node as ProseMirrorNode } from "prosemirror-model";
|
||||
import { PlainTextSerializer } from "../types";
|
||||
|
||||
/**
|
||||
* Returns the text content between two positions.
|
||||
*
|
||||
* @param doc The Prosemirror document to use
|
||||
* @param from A start point
|
||||
* @param to An end point
|
||||
* @param plainTextSerializers A map of node names to PlainTextSerializers which convert a node to plain text
|
||||
* @returns A string of plain text
|
||||
*/
|
||||
export default function textBetween(
|
||||
doc: ProseMirrorNode,
|
||||
from: number,
|
||||
to: number,
|
||||
plainTextSerializers: Record<string, PlainTextSerializer | undefined>
|
||||
): string {
|
||||
const blockSeparator = "\n\n";
|
||||
let text = "";
|
||||
let separated = true;
|
||||
|
||||
doc.nodesBetween(from, to, (node, pos) => {
|
||||
const toPlainText = plainTextSerializers[node.type.name];
|
||||
|
||||
if (toPlainText) {
|
||||
if (node.isBlock && !separated) {
|
||||
text += blockSeparator;
|
||||
separated = true;
|
||||
}
|
||||
|
||||
text += toPlainText(node);
|
||||
} else if (node.isText) {
|
||||
text += node.text?.slice(Math.max(from, pos) - pos, to - pos);
|
||||
separated = false;
|
||||
} else if (node.isBlock && !separated) {
|
||||
text += blockSeparator;
|
||||
separated = true;
|
||||
}
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
@@ -63,6 +63,7 @@ export default class Attachment extends Node {
|
||||
node.attrs.title,
|
||||
];
|
||||
},
|
||||
toPlainText: (node) => node.attrs.title,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export default class Embed extends Node {
|
||||
{ class: "embed", src: node.attrs.href, contentEditable: "false" },
|
||||
0,
|
||||
],
|
||||
toPlainText: (node) => node.attrs.href,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ export default class Emoji extends Node {
|
||||
const text = document.createTextNode(`:${node.attrs["data-name"]}:`);
|
||||
return ["span", { class: "emoji" }, text];
|
||||
},
|
||||
toPlainText: (node) => nameToEmoji[node.attrs["data-name"]],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,8 @@ export default class HardBreak extends Node {
|
||||
group: "inline",
|
||||
selectable: false,
|
||||
parseDOM: [{ tag: "br" }],
|
||||
toDOM() {
|
||||
return ["br"];
|
||||
},
|
||||
toDOM: () => ["br"],
|
||||
toPlainText: () => "\n",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { InputRule } from "prosemirror-inputrules";
|
||||
import { TokenConfig } from "prosemirror-markdown";
|
||||
import {
|
||||
Node as ProsemirrorNode,
|
||||
NodeSpec,
|
||||
Node as ProsemirrorNode,
|
||||
NodeType,
|
||||
Schema,
|
||||
} from "prosemirror-model";
|
||||
|
||||
@@ -13,6 +13,7 @@ import Image from "../nodes/Image";
|
||||
import Node from "../nodes/Node";
|
||||
import Paragraph from "../nodes/Paragraph";
|
||||
import Text from "../nodes/Text";
|
||||
import ClipboardTextSerializer from "../plugins/ClipboardTextSerializer";
|
||||
import DateTime from "../plugins/DateTime";
|
||||
import History from "../plugins/History";
|
||||
import MaxLength from "../plugins/MaxLength";
|
||||
@@ -41,6 +42,7 @@ const basicPackage: (typeof Node | typeof Mark | typeof Extension)[] = [
|
||||
Placeholder,
|
||||
MaxLength,
|
||||
DateTime,
|
||||
ClipboardTextSerializer,
|
||||
];
|
||||
|
||||
export default basicPackage;
|
||||
|
||||
38
shared/editor/plugins/ClipboardTextSerializer.ts
Normal file
38
shared/editor/plugins/ClipboardTextSerializer.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Plugin, PluginKey } from "prosemirror-state";
|
||||
import Extension from "../lib/Extension";
|
||||
import textBetween from "../lib/textBetween";
|
||||
|
||||
/**
|
||||
* A plugin that allows overriding the default behavior of the editor to allow
|
||||
* copying text for nodes that do not inherently have text children by defining
|
||||
* a `toPlainText` method in the node spec.
|
||||
*/
|
||||
export default class ClipboardTextSerializer extends Extension {
|
||||
get name() {
|
||||
return "clipboardTextSerializer";
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
const textSerializers = Object.fromEntries(
|
||||
Object.entries(this.editor.schema.nodes)
|
||||
.filter(([, node]) => node.spec.toPlainText)
|
||||
.map(([name, node]) => [name, node.spec.toPlainText])
|
||||
);
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("clipboardTextSerializer"),
|
||||
props: {
|
||||
clipboardTextSerializer: () => {
|
||||
const { doc, selection } = this.editor.view.state;
|
||||
const { ranges } = selection;
|
||||
const from = Math.min(...ranges.map((range) => range.$from.pos));
|
||||
const to = Math.max(...ranges.map((range) => range.$to.pos));
|
||||
|
||||
return textBetween(doc, from, to, textSerializers);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { EditorState, Transaction } from "prosemirror-state";
|
||||
import * as React from "react";
|
||||
import { DefaultTheme } from "styled-components";
|
||||
|
||||
export type PlainTextSerializer = (node: ProsemirrorNode) => string;
|
||||
|
||||
export enum EventType {
|
||||
blockMenuOpen = "blockMenuOpen",
|
||||
blockMenuClose = "blockMenuClose",
|
||||
|
||||
8
shared/typings/prosemirror-model.d.ts
vendored
8
shared/typings/prosemirror-model.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
import { PlainTextSerializer } from "../editor/types";
|
||||
import "prosemirror-model";
|
||||
|
||||
declare module "prosemirror-model" {
|
||||
@@ -7,4 +8,11 @@ declare module "prosemirror-model" {
|
||||
// https://github.com/ProseMirror/prosemirror-model/blob/bd13a2329fda39f1c4d09abd8f0db2032bdc8014/src/replace.js#L51
|
||||
removeBetween(from: number, to: number): Slice;
|
||||
}
|
||||
|
||||
interface NodeSpec {
|
||||
/**
|
||||
* Defines the text representation of the node when copying to clipboard.
|
||||
*/
|
||||
toPlainText?: PlainTextSerializer;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user