diff --git a/.changeset/quiet-months-argue.md b/.changeset/quiet-months-argue.md new file mode 100644 index 00000000000..9db967f66fa --- /dev/null +++ b/.changeset/quiet-months-argue.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/fields-document': patch +--- + +Adds support for pasting a url onto text to create a link diff --git a/packages/fields-document/src/DocumentEditor/pasting/index.ts b/packages/fields-document/src/DocumentEditor/pasting/index.ts index e5e22b500c1..3756787133e 100644 --- a/packages/fields-document/src/DocumentEditor/pasting/index.ts +++ b/packages/fields-document/src/DocumentEditor/pasting/index.ts @@ -1,8 +1,11 @@ -import { Descendant, Editor, Transforms } from 'slate'; +import { Descendant, Editor, Transforms, Range } from 'slate'; +import { isValidURL } from '../isValidURL'; import { insertNodesButReplaceIfSelectionIsAtEmptyParagraphOrHeading } from '../utils'; import { deserializeHTML } from './html'; import { deserializeMarkdown } from './markdown'; +const urlPattern = /https?:\/\//; + function insertFragmentButDifferent(editor: Editor, nodes: Descendant[]) { if (Editor.isBlock(editor, nodes[0])) { insertNodesButReplaceIfSelectionIsAtEmptyParagraphOrHeading(editor, nodes); @@ -63,6 +66,29 @@ export function withPasting(editor: Editor): Editor { } } + const plain = data.getData('text/plain'); + + if ( + // isValidURL is a bit more permissive than a user might expect + // so for pasting, we'll constrain it to starting with https:// or http:// + urlPattern.test(plain) && + isValidURL(plain) && + editor.selection && + !Range.isCollapsed(editor.selection) && + // we only want to turn the selected text into a link if the selection is within the same block + Editor.above(editor, { + match: node => Editor.isBlock(editor, node) && !Editor.isBlock(editor, node.children[0]), + }) && + // and there is only text(potentially with marks) in the selection + // no other links or inline relationships + Editor.nodes(editor, { + match: node => Editor.isInline(editor, node), + }).next().done + ) { + Transforms.wrapNodes(editor, { type: 'link', href: plain, children: [] }, { split: true }); + return; + } + const html = data.getData('text/html'); if (html) { const fragment = deserializeHTML(html); @@ -70,7 +96,6 @@ export function withPasting(editor: Editor): Editor { return; } - const plain = data.getData('text/plain'); if (plain) { const fragment = deserializeMarkdown(plain); insertFragmentButDifferent(editor, fragment); diff --git a/packages/fields-document/src/DocumentEditor/pasting/links.test.tsx b/packages/fields-document/src/DocumentEditor/pasting/links.test.tsx new file mode 100644 index 00000000000..30fd97f57b2 --- /dev/null +++ b/packages/fields-document/src/DocumentEditor/pasting/links.test.tsx @@ -0,0 +1,132 @@ +/** @jest-environment jsdom */ +/** @jsxRuntime classic */ +/** @jsx jsx */ +import { Editor } from 'slate'; +import { makeEditor, jsx } from '../tests/utils'; +import { MyDataTransfer } from './data-transfer'; + +function pasteText(editor: Editor, text: string) { + const data = new MyDataTransfer(); + data.setData('text/plain', text); + editor.insertData(data); +} + +test('pasting a url on some text wraps the text with a link', () => { + const editor = makeEditor( + + + + blah + blah + blah + + + + ); + pasteText(editor, 'https://keystonejs.com'); + expect(editor).toMatchInlineSnapshot(` + + + + blah + + + + + blah + + + + + blah + + + + `); +}); + +test('pasting a url on a selection spanning multiple blocks replaces the selection with the url', () => { + const editor = makeEditor( + + + + start should still exist + blah blah + + + + + blah blah + end should still exist + + + + ); + pasteText(editor, 'https://keystonejs.com'); + expect(editor).toMatchInlineSnapshot(` + + + + start should still exist + + + + https://keystonejs.com + + + + + end should still exist + + + + `); +}); + +test('pasting a url on a selection with a link inside replaces the selection with the url', () => { + const editor = makeEditor( + + + + start should still exist + should{' '} + + + be + + + replaced + end should still exist + + + + ); + pasteText(editor, 'https://keystonejs.com'); + expect(editor).toMatchInlineSnapshot(` + + + + start should still exist + + + + https://keystonejs.com + + + + + end should still exist + + + + `); +});