mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
fix: keep blank lines when pasting plain text into the composer
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { Editor } from '@tiptap/core';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
|
||||
import { PlainTextPaste } from '../plain-text-paste';
|
||||
|
||||
function makeEditor(content = '<p></p>') {
|
||||
return new Editor({
|
||||
element: document.createElement('div'),
|
||||
extensions: [StarterKit, PlainTextPaste],
|
||||
coreExtensionOptions: { clipboardTextSerializer: { blockSeparator: '\n' } },
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
// jsdom has no ClipboardEvent, which pasteText constructs when given none.
|
||||
function paste(editor: Editor, text: string) {
|
||||
editor.view.pasteText(text, new Event('paste') as ClipboardEvent);
|
||||
}
|
||||
|
||||
describe('plain-text paste', () => {
|
||||
it('keeps blank lines as empty paragraphs', () => {
|
||||
const editor = makeEditor();
|
||||
paste(editor, 'Hi Joost,\n\nThanks for the write-up.\n\n\nPHASE 1');
|
||||
expect(editor.getHTML()).toBe(
|
||||
'<p>Hi Joost,</p><p></p><p>Thanks for the write-up.</p><p></p><p></p><p>PHASE 1</p>',
|
||||
);
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
it('treats CRLF and CR line endings like LF', () => {
|
||||
const editor = makeEditor();
|
||||
paste(editor, 'one\r\n\r\ntwo\rthree');
|
||||
expect(editor.getHTML()).toBe('<p>one</p><p></p><p>two</p><p>three</p>');
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
it('pastes a single line inline without splitting the paragraph', () => {
|
||||
const editor = makeEditor('<p>Hello world</p>');
|
||||
editor.commands.setTextSelection(7);
|
||||
paste(editor, 'big ');
|
||||
expect(editor.getHTML()).toBe('<p>Hello big world</p>');
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
it('copies back out as the same text', () => {
|
||||
const text = 'Hi Joost,\n\nThanks for the write-up.\n\n\nPHASE 1';
|
||||
const editor = makeEditor();
|
||||
paste(editor, text);
|
||||
// TipTap's serializer reads the editor selection, not the slice it gets.
|
||||
editor.commands.selectAll();
|
||||
const slice = editor.state.selection.content();
|
||||
const copied = editor.view.someProp('clipboardTextSerializer', (f) => f(slice, editor.view));
|
||||
expect(copied).toBe(text);
|
||||
editor.destroy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
import { Fragment, Slice } from "@tiptap/pm/model";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
|
||||
/**
|
||||
* Pastes plain text as one paragraph per line, blank lines included.
|
||||
*
|
||||
* In the composer Enter starts a new paragraph and a blank line is an empty
|
||||
* paragraph. ProseMirror's default text paste splits on runs of newlines
|
||||
* instead, so every blank line was dropped and a pasted mail arrived as a
|
||||
* wall of lines with no gap between its paragraphs. This builds the same
|
||||
* paragraphs typing the text would.
|
||||
*/
|
||||
export const PlainTextPaste = Extension.create({
|
||||
name: "plainTextPaste",
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const { schema } = this.editor;
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("plainTextPaste"),
|
||||
props: {
|
||||
clipboardTextParser: (text, $context) => {
|
||||
const marks = $context.marks();
|
||||
const paragraphs = text
|
||||
.split(/\r\n?|\n/)
|
||||
.map((line) =>
|
||||
schema.nodes.paragraph.create(null, line ? schema.text(line, marks) : null),
|
||||
);
|
||||
return Slice.maxOpen(Fragment.from(paragraphs));
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import Underline from "@tiptap/extension-underline";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import TextAlign from "@tiptap/extension-text-align";
|
||||
import { TextDirection } from "@/components/email/text-direction";
|
||||
import { PlainTextPaste } from "@/components/email/plain-text-paste";
|
||||
import { TextStyle, BackgroundColor } from "@tiptap/extension-text-style";
|
||||
import Color from "@tiptap/extension-color";
|
||||
import { FontSize, FONT_SIZES } from "@/components/email/font-size";
|
||||
@@ -236,7 +237,14 @@ export function RichTextEditor({
|
||||
// into editable content (#822).
|
||||
SignatureBlock.configure({ editHint: tComposer('signature_edit_hint') }),
|
||||
TextDirection,
|
||||
PlainTextPaste,
|
||||
],
|
||||
// One newline per paragraph when copying out as plain text, the inverse
|
||||
// of PlainTextPaste: a blank line is an empty paragraph, so the default
|
||||
// "\n\n" separator doubled every line break.
|
||||
coreExtensionOptions: {
|
||||
clipboardTextSerializer: { blockSeparator: "\n" },
|
||||
},
|
||||
content,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
|
||||
Reference in New Issue
Block a user