diff --git a/.vscode/settings.json b/.vscode/settings.json index f89d258263c..77c9a5dc7b8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -60,5 +60,8 @@ "editor.defaultFormatter": "dbaeumer.vscode-eslint", "[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[javascript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" } } diff --git a/demos/src/Marks/Link/React/index.spec.js b/demos/src/Marks/Link/React/index.spec.js index db43732b5a0..a34a7c9e99f 100644 --- a/demos/src/Marks/Link/React/index.spec.js +++ b/demos/src/Marks/Link/React/index.spec.js @@ -71,6 +71,14 @@ context('/src/Marks/Link/React/', () => { .should('have.attr', 'href', 'https://example.com') }) + it('detects a pasted URL with query params', () => { + cy.get('.ProseMirror') + .paste({ pastePayload: 'https://example.com?paramA=nice¶mB=cool', pasteType: 'text/plain' }) + .find('a') + .should('contain', 'Example Text') + .should('have.attr', 'href', 'https://example.com?paramA=nice¶mB=cool') + }) + it('correctly detects multiple pasted URLs', () => { cy.get('.ProseMirror').paste({ pastePayload: diff --git a/demos/src/Marks/Link/Vue/index.spec.js b/demos/src/Marks/Link/Vue/index.spec.js index 7f80fb60550..a3b33016599 100644 --- a/demos/src/Marks/Link/Vue/index.spec.js +++ b/demos/src/Marks/Link/Vue/index.spec.js @@ -61,6 +61,14 @@ context('/src/Marks/Link/Vue/', () => { .should('have.attr', 'href', 'https://example.com') }) + it('detects a pasted URL with query params', () => { + cy.get('.ProseMirror') + .paste({ pastePayload: 'https://example.com?paramA=nice¶mB=cool', pasteType: 'text/plain' }) + .find('a') + .should('contain', 'Example Text') + .should('have.attr', 'href', 'https://example.com?paramA=nice¶mB=cool') + }) + it('correctly detects multiple pasted URLs', () => { cy.get('.ProseMirror').paste({ pastePayload: 'https://example1.com, https://example2.com/foobar, (http://example3.com/foobar)', pasteType: 'text/plain' }) diff --git a/packages/extension-link/src/helpers/autolink.ts b/packages/extension-link/src/helpers/autolink.ts index 123a610c29b..643a76f0a61 100644 --- a/packages/extension-link/src/helpers/autolink.ts +++ b/packages/extension-link/src/helpers/autolink.ts @@ -29,6 +29,7 @@ export function autolink(options: AutolinkOptions): Plugin { const transform = combineTransactionSteps(oldState.doc, [...transactions]) const { mapping } = transform const changes = getChangedRanges(transform) + let needsAutolink = true changes.forEach(({ oldRange, newRange }) => { // at first we check if we have to remove links @@ -51,13 +52,21 @@ export function autolink(options: AutolinkOptions): Plugin { const wasLink = test(oldLinkText) const isLink = test(newLinkText) + if (wasLink) { + needsAutolink = false + } + // remove only the link, if it was a link before too // because we don’t want to remove links that were set manually if (wasLink && !isLink) { - tr.removeMark(newMark.from, newMark.to, options.type) + tr.removeMark(needsAutolink ? newMark.from : newMark.to - 1, newMark.to, options.type) } }) + if (!needsAutolink) { + return + } + // now let’s see if we can add new links const nodesInChangedRanges = findChildrenInRange( newState.doc, diff --git a/packages/extension-link/src/helpers/pasteHandler.ts b/packages/extension-link/src/helpers/pasteHandler.ts index ed03e6903a1..0d4c834032c 100644 --- a/packages/extension-link/src/helpers/pasteHandler.ts +++ b/packages/extension-link/src/helpers/pasteHandler.ts @@ -1,11 +1,12 @@ import { Editor } from '@tiptap/core' -import { MarkType } from '@tiptap/pm/model' +import { Mark, MarkType } from '@tiptap/pm/model' import { Plugin, PluginKey } from '@tiptap/pm/state' import { find } from 'linkifyjs' type PasteHandlerOptions = { editor: Editor type: MarkType + linkOnPaste?: boolean } export function pasteHandler(options: PasteHandlerOptions): Plugin { @@ -15,11 +16,18 @@ export function pasteHandler(options: PasteHandlerOptions): Plugin { handlePaste: (view, event, slice) => { const { state } = view const { selection } = state - const { empty } = selection - if (empty) { - return false - } + const pastedLinkMarks: Mark[] = [] + + slice.content.forEach(node => { + node.marks.forEach(mark => { + if (mark.type.name === options.type.name) { + pastedLinkMarks.push(mark) + } + }) + }) + + const hasPastedLink = pastedLinkMarks.length > 0 let textContent = '' @@ -29,15 +37,65 @@ export function pasteHandler(options: PasteHandlerOptions): Plugin { const link = find(textContent).find(item => item.isLink && item.value === textContent) - if (!textContent || !link) { + if (!selection.empty && options.linkOnPaste) { + const pastedLink = hasPastedLink ? pastedLinkMarks[0].attrs.href : link?.href || null + + if (pastedLink) { + options.editor.commands.setMark(options.type, { + href: pastedLink, + }) + return true + } + } + + if (slice.content.firstChild?.type.name === 'text' && slice.content.firstChild?.marks.some(mark => mark.type.name === options.type.name)) { return false } - options.editor.commands.setMark(options.type, { - href: link.href, + if (link && selection.empty) { + options.editor.commands.insertContent(`${link.href}`) + return true + } + + const { tr } = state + let deleteOnly = false + + if (!selection.empty) { + deleteOnly = true + tr.delete(selection.from, selection.to) + } + + let currentPos = selection.from + + slice.content.forEach(node => { + const fragmentLinks = find(node.textContent) + + tr.insert(currentPos - 1, node) + + if (fragmentLinks.length > 0) { + deleteOnly = false + + fragmentLinks.forEach(fragmentLink => { + const linkStart = currentPos + fragmentLink.start + const linkEnd = currentPos + fragmentLink.end + + const hasMark = tr.doc.rangeHasMark(linkStart, linkEnd, options.type) + + if (!hasMark) { + tr.addMark(linkStart, linkEnd, options.type.create({ href: fragmentLink.href })) + } + }) + + } + currentPos += node.nodeSize }) - return true + if (tr.docChanged && !deleteOnly) { + options.editor.view.dispatch(tr) + return true + } + + return false }, }, }) diff --git a/packages/extension-link/src/link.ts b/packages/extension-link/src/link.ts index e7898dea132..dde70e53e9d 100644 --- a/packages/extension-link/src/link.ts +++ b/packages/extension-link/src/link.ts @@ -1,6 +1,6 @@ -import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core' +import { Mark, mergeAttributes } from '@tiptap/core' import { Plugin } from '@tiptap/pm/state' -import { find, registerCustomProtocol, reset } from 'linkifyjs' +import { registerCustomProtocol, reset } from 'linkifyjs' import { autolink } from './helpers/autolink' import { clickHandler } from './helpers/clickHandler' @@ -146,31 +146,6 @@ export const Link = Mark.create({ } }, - addPasteRules() { - return [ - markPasteRule({ - find: text => find(text) - .filter(link => { - if (this.options.validate) { - return this.options.validate(link.value) - } - - return true - }) - .filter(link => link.isLink) - .map(link => ({ - text: link.value, - index: link.start, - data: link, - })), - type: this.type, - getAttributes: match => ({ - href: match.data?.href, - }), - }), - ] - }, - addProseMirrorPlugins() { const plugins: Plugin[] = [] @@ -191,14 +166,13 @@ export const Link = Mark.create({ ) } - if (this.options.linkOnPaste) { - plugins.push( - pasteHandler({ - editor: this.editor, - type: this.type, - }), - ) - } + plugins.push( + pasteHandler({ + editor: this.editor, + type: this.type, + linkOnPaste: this.options.linkOnPaste, + }), + ) return plugins },