Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(extension-link): fix link not being kept when pasting url with link #3975

Merged
merged 8 commits into from
Apr 26, 2023
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,8 @@
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
}
8 changes: 8 additions & 0 deletions demos/src/Marks/Link/React/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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&paramB=cool', pasteType: 'text/plain' })
.find('a')
.should('contain', 'Example Text')
.should('have.attr', 'href', 'https://example.com?paramA=nice&paramB=cool')
})

it('correctly detects multiple pasted URLs', () => {
cy.get('.ProseMirror').paste({
pastePayload:
Expand Down
8 changes: 8 additions & 0 deletions demos/src/Marks/Link/Vue/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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&paramB=cool', pasteType: 'text/plain' })
.find('a')
.should('contain', 'Example Text')
.should('have.attr', 'href', 'https://example.com?paramA=nice&paramB=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' })

Expand Down
11 changes: 10 additions & 1 deletion packages/extension-link/src/helpers/autolink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
76 changes: 67 additions & 9 deletions packages/extension-link/src/helpers/pasteHandler.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 = ''

Expand All @@ -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(`<a href="${link.href}">${link.href}</a>`)
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
},
},
})
Expand Down
44 changes: 9 additions & 35 deletions packages/extension-link/src/link.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -146,31 +146,6 @@ export const Link = Mark.create<LinkOptions>({
}
},

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[] = []

Expand All @@ -191,14 +166,13 @@ export const Link = Mark.create<LinkOptions>({
)
}

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
},
Expand Down