Fix/link pasting (#4700)

* revert link paste handling to behavior before

* fix(link): fix linking while typing

* add validate support for autolinking back

* revert autolink behaviour

* fix autolinking on pasting text

* remove broken link

* fix react link test

* fix savvy test

---------

Co-authored-by: bdbch <dominik@bdbch.com>
This commit is contained in:
bdbch 2024-01-08 12:48:14 +01:00 committed by GitHub
parent 5aa9051881
commit eaee9c7177
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 71 additions and 50 deletions

View File

@ -24,7 +24,7 @@ context('/src/Examples/Savvy/React/', () => {
tests.forEach(test => {
it(`should parse ${test[0]} correctly`, () => {
cy.get('.tiptap').type(test[0]).should('contain', test[1])
cy.get('.tiptap').type(`${test[0]} `).should('contain', test[1])
})
})

View File

@ -25,7 +25,7 @@ context('/src/Examples/Savvy/Vue/', () => {
tests.forEach(test => {
it(`should parse ${test[0]} correctly`, () => {
cy.get('.tiptap')
.type(test[0])
.type(`${test[0]} `)
.should('contain', test[1])
})
})

View File

@ -17,6 +17,7 @@ export default () => {
Code,
Link.configure({
openOnClick: false,
autolink: true,
}),
],
content: `

View File

@ -1,5 +1,3 @@
import Link from '@tiptap/extension-link'
context('/src/Marks/Link/React/', () => {
before(() => {
cy.visit('/src/Marks/Link/React/')
@ -12,18 +10,6 @@ context('/src/Marks/Link/React/', () => {
})
})
it('should add a custom class to a link', () => {
const linkExtension = Link.configure({
HTMLAttributes: {
class: 'foo',
},
})
expect(linkExtension.options.HTMLAttributes).to.deep.include({
class: 'foo',
})
})
it('should parse a tags correctly', () => {
cy.get('.tiptap').then(([{ editor }]) => {
editor.commands.setContent('<p><a href="#">Example Text</a></p>')
@ -66,6 +52,16 @@ context('/src/Marks/Link/React/', () => {
})
})
it('detects autolinking', () => {
cy.get('.tiptap').type('https://example.com ').find('a').should('contain', 'https://example.com')
.should('have.attr', 'href', 'https://example.com')
})
it('detects autolinking with numbers', () => {
cy.get('.tiptap').type('https://tiptap4u.com ').find('a').should('contain', 'https://tiptap4u.com')
.should('have.attr', 'href', 'https://tiptap4u.com')
})
it('detects a pasted URL within a text', () => {
cy.get('.tiptap')
.paste({

View File

@ -61,6 +61,16 @@ context('/src/Marks/Link/Vue/', () => {
.should('have.attr', 'href', 'https://example.com')
})
it('detects autolinking', () => {
cy.get('.tiptap').type('https://example.com ').find('a').should('contain', 'https://example.com')
.should('have.attr', 'href', 'https://example.com')
})
it('detects autolinking with numbers', () => {
cy.get('.tiptap').type('https://tiptap4u.com ').find('a').should('contain', 'https://tiptap4u.com')
.should('have.attr', 'href', 'https://tiptap4u.com')
})
it('detects a pasted URL with query params', () => {
cy.get('.tiptap')
.paste({ pastePayload: 'https://example.com?paramA=nice&paramB=cool', pasteType: 'text/plain' })

View File

@ -21,7 +21,7 @@ export type PasteRuleMatch = {
data?: Record<string, any>
}
export type PasteRuleFinder = RegExp | ((text: string) => PasteRuleMatch[] | null | undefined)
export type PasteRuleFinder = RegExp | ((text: string, event?: ClipboardEvent) => PasteRuleMatch[] | null | undefined)
export class PasteRule {
find: PasteRuleFinder
@ -58,12 +58,13 @@ export class PasteRule {
const pasteRuleMatcherHandler = (
text: string,
find: PasteRuleFinder,
event?: ClipboardEvent,
): ExtendedRegExpMatchArray[] => {
if (isRegExp(find)) {
return [...text.matchAll(find)]
}
const matches = find(text)
const matches = find(text, event)
if (!matches) {
return []
@ -119,7 +120,7 @@ function run(config: {
const resolvedTo = Math.min(to, pos + node.content.size)
const textToMatch = node.textBetween(resolvedFrom - pos, resolvedTo - pos, undefined, '\ufffc')
const matches = pasteRuleMatcherHandler(textToMatch, rule.find)
const matches = pasteRuleMatcherHandler(textToMatch, rule.find, pasteEvent)
matches.forEach(match => {
if (match.index === undefined) {

View File

@ -26,6 +26,10 @@ export function getMarksBetween(from: number, to: number, doc: ProseMirrorNode):
})
} else {
doc.nodesBetween(from, to, (node, pos) => {
if (!node || node.nodeSize === undefined) {
return
}
marks.push(
...node.marks.map(mark => ({
from: pos,

View File

@ -33,16 +33,8 @@ export function pasteHandler(options: PasteHandlerOptions): Plugin {
return false
}
const html = event.clipboardData?.getData('text/html')
const hrefRegex = /href="([^"]*)"/
const existingLink = html?.match(hrefRegex)
const url = existingLink ? existingLink[1] : link.href
options.editor.commands.setMark(options.type, {
href: url,
href: link.href,
})
return true

View File

@ -1,4 +1,6 @@
import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'
import {
Mark, markPasteRule, mergeAttributes, PasteRuleMatch,
} from '@tiptap/core'
import { Plugin } from '@tiptap/pm/state'
import { find, registerCustomProtocol, reset } from 'linkifyjs'
@ -11,6 +13,8 @@ export interface LinkProtocolOptions {
optionalSlashes?: boolean;
}
export const pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi
export interface LinkOptions {
/**
* If enabled, it adds links as you type.
@ -158,33 +162,46 @@ 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)
}
find: (text, event) => {
const html = event?.clipboardData?.getData('text/html')
return true
})
.filter(link => link.isLink)
.map(link => ({
text: link.value,
index: link.start,
data: link,
})),
type: this.type,
getAttributes: (match, pasteEvent) => {
const html = pasteEvent?.clipboardData?.getData('text/html')
const hrefRegex = /href="([^"]*)"/
const foundLinks: PasteRuleMatch[] = []
const existingLink = html?.match(hrefRegex)
if (html) {
const dom = new DOMParser().parseFromString(html, 'text/html')
const anchors = dom.querySelectorAll('a')
if (existingLink) {
return {
href: existingLink[1],
if (anchors.length) {
[...anchors].forEach(anchor => (foundLinks.push({
text: anchor.innerText,
data: {
href: anchor.getAttribute('href'),
},
// get the index of the anchor inside the text
// and add the length of the anchor text
index: dom.body.innerText.indexOf(anchor.innerText) + anchor.innerText.length,
})))
}
}
if (text) {
const links = find(text).filter(item => item.isLink)
if (links.length) {
links.forEach(link => (foundLinks.push({
text: link.value,
data: {
href: link.href,
},
index: link.start,
})))
}
}
return foundLinks
},
type: this.type,
getAttributes: match => {
return {
href: match.data?.href,
}