diff --git a/app/scenes/Document/components/EditableTitle.tsx b/app/scenes/Document/components/EditableTitle.tsx index 998f51286..4c4de2390 100644 --- a/app/scenes/Document/components/EditableTitle.tsx +++ b/app/scenes/Document/components/EditableTitle.tsx @@ -1,8 +1,12 @@ import { observer } from "mobx-react"; +import { Slice } from "prosemirror-model"; import { Selection } from "prosemirror-state"; +import { __parseFromClipboard } from "prosemirror-view"; import * as React from "react"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; +import isMarkdown from "@shared/editor/lib/isMarkdown"; +import normalizePastedMarkdown from "@shared/editor/lib/markdown/normalize"; import { light } from "@shared/styles/theme"; import { getCurrentDateAsString, @@ -120,9 +124,12 @@ const EditableTitle = React.forwardRef( const handlePaste = React.useCallback( (event: React.ClipboardEvent) => { event.preventDefault(); + const text = event.clipboardData.getData("text/plain"); + const html = event.clipboardData.getData("text/html"); const [firstLine, ...rest] = text.split(`\n`); const content = rest.join(`\n`).trim(); + window.document.execCommand( "insertText", false, @@ -130,11 +137,37 @@ const EditableTitle = React.forwardRef( ); if (editor && content) { - const { view, parser } = editor; + const { view, pasteParser } = editor; + let slice; + + if (isMarkdown(text)) { + const paste = pasteParser.parse(normalizePastedMarkdown(content)); + slice = paste.slice(0); + } else { + const defaultSlice = __parseFromClipboard( + view, + text, + html, + false, + view.state.selection.$from + ); + + // remove first node from slice + slice = defaultSlice.content.firstChild + ? new Slice( + defaultSlice.content.cut( + defaultSlice.content.firstChild.nodeSize + ), + defaultSlice.openStart, + defaultSlice.openEnd + ) + : defaultSlice; + } + view.dispatch( view.state.tr .setSelection(Selection.atStart(view.state.doc)) - .insert(0, parser.parse(content)) + .replaceSelection(slice) ); } }, diff --git a/shared/editor/lib/isMarkdown.ts b/shared/editor/lib/isMarkdown.ts index 1b660ae51..680410c88 100644 --- a/shared/editor/lib/isMarkdown.ts +++ b/shared/editor/lib/isMarkdown.ts @@ -19,7 +19,7 @@ export default function isMarkdown(text: string): boolean { } // list-ish - const listItems = text.match(/^[\d-*].?\s\S+/gm); + const listItems = text.match(/^([-*]|\d+.)\s\S+/gm); if (listItems && listItems.length > 1) { return true; } diff --git a/shared/editor/lib/markdown/normalize.ts b/shared/editor/lib/markdown/normalize.ts new file mode 100644 index 000000000..01b8d5554 --- /dev/null +++ b/shared/editor/lib/markdown/normalize.ts @@ -0,0 +1,22 @@ +/** + * Add support for additional syntax that users paste even though it isn't + * supported by the markdown parser directly by massaging the text content. + * + * @param text The incoming pasted plain text + */ +export default function normalizePastedMarkdown(text: string): string { + const CHECKBOX_REGEX = /^\s?(\[(X|\s|_|-)\]\s(.*)?)/gim; + + // find checkboxes not contained in a list and wrap them in list items + while (text.match(CHECKBOX_REGEX)) { + text = text.replace(CHECKBOX_REGEX, (match) => `- ${match.trim()}`); + } + + // find multiple newlines and insert a hard break to ensure they are respected + text = text.replace(/\n{3,}/g, "\n\n\\\n"); + + // find single newlines and insert an extra to ensure they are treated as paragraphs + text = text.replace(/\b\n\b/g, "\n\n"); + + return text; +} diff --git a/shared/editor/plugins/PasteHandler.ts b/shared/editor/plugins/PasteHandler.ts index 23b5e90db..909e0361d 100644 --- a/shared/editor/plugins/PasteHandler.ts +++ b/shared/editor/plugins/PasteHandler.ts @@ -4,6 +4,7 @@ import { isInTable } from "prosemirror-tables"; import { isUrl } from "../../utils/urls"; import Extension from "../lib/Extension"; import isMarkdown from "../lib/isMarkdown"; +import normalizePastedMarkdown from "../lib/markdown/normalize"; import isInCode from "../queries/isInCode"; import { LANGUAGES } from "./Prism"; @@ -13,29 +14,6 @@ function isDropboxPaper(html: string): boolean { return html?.includes("usually-unique-id"); } -/** - * Add support for additional syntax that users paste even though it isn't - * supported by the markdown parser directly by massaging the text content. - * - * @param text The incoming pasted plain text - */ -function normalizePastedMarkdown(text: string): string { - const CHECKBOX_REGEX = /^\s?(\[(X|\s|_|-)\]\s(.*)?)/gim; - - // find checkboxes not contained in a list and wrap them in list items - while (text.match(CHECKBOX_REGEX)) { - text = text.replace(CHECKBOX_REGEX, (match) => `- ${match.trim()}`); - } - - // find multiple newlines and insert a hard break to ensure they are respected - text = text.replace(/\n{2,}/g, "\n\n\\\n"); - - // find single newlines and insert an extra to ensure they are treated as paragraphs - text = text.replace(/\b\n\b/g, "\n\n"); - - return text; -} - export default class PasteHandler extends Extension { get name() { return "markdown-paste";